Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
## **v5.5.0** — Live Is Life (2026-10-30)

### Release

- Version 5.5.0 (maintainer: a new capability, the Live View; Docker-only is parked, a possible 6.0.0): `APP_VERSION` and `CHALKER_VERSION`, Chalker cache busters (`chalker.js?v=35`, `chalker-v132`), release pages `releases/index.html` and `releases/v5.5.0.html`, `sitemap.xml`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.5.0.md`. `llms.txt` (the upload marker, the live copy and the Live View, the import gate, and the "no spectator view" line), `automatic-backup.html` (a failed upload no longer "will not succeed"; new sections When an Upload Fails and Live View) and `rest-api.html` (live.php) are updated; the help covers Config, Analytics and the completing-and-correcting page.
- The design behind it is in `Docs/HUB-DIRECTION.md`: one-way data flow for now (the club's Tournament Manager writes, the server and Analytics only display), and the debt test the Live View passed.

### Upload failure marker

- **A failed automatic upload is no longer silent** (found when the 2026-10-26 tournament never reached the server; the likely cause was a poor connection at the club: the upload at the end of a tournament is fire-and-forget and a failure only reached the console). The outcome of every upload to the remote server is kept per tournament in its own key, `uploadStatus_<tournamentId>` (the tournament and its export are unchanged; no key means "nothing known", so tournaments from before this behave as they did). A finished tournament whose upload failed shows **Not uploaded to <server>: <reason>** with **Upload now** on Setup's current tournament, and a **Not uploaded** chip in the bracket page's status line. A successful upload shows "Uploaded to <server> at <time>" on Setup.
- **The reason is the real one:** the relay could not reach the server ("Could not resolve host"), this computer's server did not answer or has no relay (the page opened as a file), the server failed, or it refused (wrong API key: "Check the API key under Global Settings → Remote backup", a host not allowed, too large).
- **It mends itself when it can.** Network, relay and server failures are tried again while the page is open: after 30 s, 1 min, 2 min, then every 5 minutes, at once when the browser reports it is online again, and when the page is opened with a failure waiting. A refusal is not retried; it waits for someone to fix the cause, and **Upload now** tries it at once and tells you what happened. Repeating is safe: the upload overwrites the same file.
- **Setup's tournament list tags the tournaments that never made it:** a finished tournament whose upload failed carries an amber **Not uploaded** tag on its row (hover: the reason), so anyone can find it, load it and press **Upload now**, also after another tournament has been created. The tag goes when the upload succeeds, and does not show for a record about a remote that is no longer the configured one.
- **Only for the remote server.** The local server is this computer's own, and not having one is normal when the page is opened as a file. A status about an address that is no longer the configured remote is ignored. Deleting a tournament forgets its status. The retry follows the loaded tournament only: one that is not loaded is retried when it is loaded again, and the tag on its row says which one that is (Docs/HUB-DIRECTION.md, step 1).
- Tested in the browser against two local containers (the club's with the working tree, a remote with an API key): the remote down (the real 502 from the relay, chip and note, retry scheduled), the remote started (retry succeeded on its own, chip gone), a wrong key (refused, plain message, no retry), the key fixed and Upload now (uploaded, note updated), the page reloaded with a failure waiting (note shown, retry resumed), the `online` event, and the remote setting removed (notice hidden).
- Files changed: `js/tournament-management.js` (`uploadToServer()`, the upload status and retry functions, `updateTournamentStatus()`, `renderSetupCurrent()`, the delete), `tournament.html`, `css/setup-page.css`, `css/bracket-view.css`, `js/dynamic-help-system.js`, `Docs/HUB-DIRECTION.md`.

### Live View

- **A tournament being played can be watched, read-only, from Analytics** (maintainer's idea, 2026-10-30: the normal bracket page and the leaderboard, going away when the tournament is over and Analytics has it). `js/live-view.js`, `css/analytics-page.css`, `css/bracket-view.css`.
- **Watch opens a list of the matches, the same on a phone and a desktop** (maintainer, 2026-10-30: the list is the default, always, with the bracket one tap away): **Live now** (with the lane), **Up next** and **Results**, newest first, each result with what it recorded ("Cat 2–1 Ben · Cat 1× 180"), under the tournament's name, date, format and how many matches are played. One column, built for a phone; matches still waiting for earlier results are only counted. **Bracket** opens the normal bracket page (read-only); there the back button reads "⬅ List" and returns to the list, and the list's own back button leaves to Analytics. **Leaderboard** opens from either. The list redraws by itself when a result or a start arrives (tested: a result and a started match on lane 1 moved into place within the poll).
- **"Live now"** at the top of the Analytics page, one line per live tournament ("ViewerCup · 2 of 14 matches played") with **Watch**; shown only while a live copy exists, refreshed when Analytics is opened and every 30 s while it is showing. A tournament this Tournament Manager is running itself is left out. `api/live.php`'s list now carries `matches` and `played` for it.
- **The bracket** (from the list's Bracket button) is the normal bracket page with the live copy, in memory, read-only: no Edit, Undo or Match Controls, only "⬅ Analytics" and the Leaderboard in the header (also in analytics mode, where the operator controls are hidden). It asks the server every 8 s (an `ETag` keeps an unchanged answer empty; paused while the tab is hidden) and redraws when something changed; the badge says "● LIVE · updated 21:14", or "Offline · last updated …". Selecting a finished match shows what it recorded ("Hal 2× 180, 3× ton"). The Leaderboard opens as usual, with "click a player to edit statistics" hidden.
- **When the tournament is over** (the live copy is gone because the final backup succeeded) the page says "This tournament has finished. See it in Analytics"; the button leaves, and Analytics takes the finished tournament in as it loads.
- **Nothing is stored and nothing of the viewer's own changes.** The live copy goes into the page's tournament, players and matches, tagged `_analyticsPreview` so nothing saves; the viewer's own tournament is put back when they leave; `localStorage` is not written (the saved page is set to Analytics, so a reload lands there). The club's point values are put into `config.points` only for the moment the leaderboard is calculated (the view toggles save the whole config, so they must never be left in it): a viewer's own value of 7 per 180 was used by nobody and put back, the club's 1 gave the points.
- **Removable:** with `js/live-view.js` deleted, the Analytics strip never shows and nothing else changes (the hooks in `main.js`, `bracket-view.js` and `bracket-rendering.js` check that `LiveView` exists).
- Tested in the browser (a club container in full mode producing the live copy, a remote in analytics mode as the member): the strip, Watch, the badge, the trimmed header, the bracket and a match's achievements, the leaderboard with the club's points, updates arriving by themselves, the hand-over at the end (live list empty, the finished tournament in the normal list, "This tournament has finished", See it in Analytics: imported, strip gone), and the viewer's own tournament restored.
- **The end is told truthfully** (found by the maintainer from a screenshot: "This tournament has finished" above "Up next · 1" and "10 of 14 played"). The live copy also goes when a tournament is reset or deleted, so its absence says nothing about finishing, and the last frame is stale. When the live copy is gone the page now asks the server's normal list: if the finished tournament is there it loads it and shows the true final results ("14 of 14 played", every result with what it recorded, the Grand Final first) under "This tournament has finished. See it in Analytics"; if not it says "This tournament is no longer live", drops the Live now and Up next groups and shows only the results "as of 21:14", with a "Back to Analytics" button. Tested both: a tournament played to the end, and one that was reset while watched.
- Not tested: a real phone (a narrow window only).
- Files changed: `js/live-view.js`, `css/live-view.css`, `api/live.php`, `js/main.js`, `js/bracket-view.js`, `js/bracket-rendering.js`, `tournament.html`, `css/analytics-page.css`, `css/bracket-view.css`, `js/dynamic-help-system.js`, `Docs/REST_API.md`, `Docs/HUB-DIRECTION.md`.

### Live copy: statistics and per-match achievements

- **The live snapshot now carries what a leaderboard needs:** each player's statistics (180s, tons, high outs, short legs, lollipops), the club's point values (`config.points`, since a viewer's own browser has its own), and for each completed match what it recorded (`achievements`, as `{ p1, p2 }`, read from the match's transaction; the history itself is still not sent). 7 KB for an 8-player double elimination with 14 matches. The setting's text says statistics are included.
- **Spike (nothing shipped): the snapshot draws the normal bracket page and the leaderboard.** Loaded in memory on the remote test container (no storage written, the member's own current tournament untouched): all 14 cards, the backside and finals, done and ready matches; the leaderboard's points, 180s, tons, high outs and legs won and lost match what the club's own leaderboard would show. A viewer needs, in memory only, `paid` set on the players (the status line counts it; payment is not sent) and the snapshot's point values in `config.points`.
- **Still to do for a viewer:** switch off "click a player to edit statistics" and the Match Controls entry in it, show a match's achievements when it is selected (the data is in the snapshot), poll and redraw, and the "Live now" strip on Analytics.
- Files changed: `js/live-upload.js`, `tournament.html` (the setting's text).

### Analytics import gate

- **Analytics no longer imports a tournament that is still being played.** `_autoImportFromDisk()` (`js/newton-history.js`) imported every tournament in the server's list once, by id, whatever its status, and recorded it as finished; so an in-progress tournament uploaded by hand (Backup to server offers the active one) was imported into every member's browser as completed, and the final upload then skipped as already there. It now skips anything whose status is not `completed` (files with no status, from before the field, are imported as before), in the list and in the file.
- Tested: a completed and an in-progress tournament on a server, Analytics opened: only the completed one imported.

### Live copy while a tournament is played

- **A slim copy of the tournament on the server after every change, off by default** (Global Settings → Server → **Live copy while it is played**; Docs/HUB-DIRECTION.md, step 3). `js/live-upload.js` sends it from `saveTournamentOnly()` (debounced 1.5 s) to `api/live.php` on this computer's own server and, through the relay with the key, on the remote one. Each send replaces the one before, so a dropped one is mended by the next; nothing is queued. Nothing is sent before the bracket is drawn.
- **The snapshot:** the format and status, the players (name, id, placement) and each match's state, score, lane and referee; no transaction history, no statistics, no payment: 1.6 KB for four players. The fields are whitelisted, since the copy is public.
- **Kept out of Analytics by construction:** it is stored in `tournaments/live/<id>.json`, a subfolder the tournament list never reads (as `registry/` and `corrections/`). When the finished tournament's backup succeeds, the live copy on that server is removed (the final copy is authoritative); a failed backup leaves it, showing the finished state. A reset or deleted tournament's goes too, an older revision arriving late is ignored, and a live copy not written to for three days is removed.
- **The setting says what it does:** anyone who can open the server can read a live copy, as with everything else there. No viewer yet: the data is there for one (the debt test in `Docs/HUB-DIRECTION.md`).
- Removable: with the setting off, or `js/live-upload.js` deleted, nothing changes.
- Tested in the browser against two local containers (club and remote with an API key): nothing sent before the draw; after the draw, a started match and a result on both servers; the live copy gone and the final in the normal list when the backup succeeded; reset removed both; endpoint: key required, a stale write ignored, a path-traversal id refused, `ETag` and `304`, never in the tournament list.
- Files changed: `api/live.php`, `js/live-upload.js`, `js/tournament-management.js` (the hooks), `js/results-config.js`, `js/types.js`, `tournament.html`, `js/newton-history.js`, `Docs/REST_API.md`, `Docs/HUB-DIRECTION.md`.

## **v5.4.8** — Non, je ne regrette rien (2026-10-23)

### Release

- Version 5.4.8: `APP_VERSION` and `CHALKER_VERSION`, Chalker cache busters (`chalker.js?v=34`, `chalker-v131`), release pages `releases/index.html` and `releases/v5.4.8.html`, `sitemap.xml`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.4.8.md`. `llms.txt` and the help (bracket page, Completing & Correcting Matches) mention Edit match.

### Analytics register: match achievements at tournament end

- **Found while checking that an edit reaches Analytics:** at the end of a tournament `reconcileMatchAchievements()` reads each match's achievements as `{ p1, p2 }`, but match completion (by hand and from the Chalker) stored them as `{ [playerId]: delta }`. It saw nothing recorded, took each player's whole tournament totals as unattributed, and added them to that player's last match under `p1`/`p2` beside the real entry: the last match then showed the player's whole tournament, and its per-match achievement points were summed over both. The last match's record was also re-saved as `live` among `final` ones (`saveMatch()` forced the status).
- **Fixed in three parts.** (1) `completeMatch()` and the edit's register save store `{ p1, p2 }` (new `NewtonDB.slotAchievements()`); the transaction keeps its per-id map, so undo is untouched. (2) Reconcile reads either shape and writes records that were stored per id back as `{ p1, p2 }`, so a tournament played partly before this change reconciles correctly; only stats nobody recorded in a match (entered outside the dialogs) go to the player's last match. (3) `saveMatch()` keeps a status the caller passes (reconcile passes `final`).
- **Not repaired:** tournaments already finalized keep their inflated last-match entries (maintainer: leave the recompute).
- Tested in the browser: a 4-player single elimination played to the end with an edit on the way and two tons entered outside any dialog: every record `{ p1, p2 }` and `final`, the edited match with its new score and achievements, the winner's last match with only its own ton, the outside tons on the other finalist's last match alone.
- Files changed: `js/newton-db.js` (`slotAchievements()`, `saveMatch()`, `reconcileMatchAchievements()`), `js/clean-match-progression.js` (`completeMatch()`), `js/bracket-rendering.js` (`_saveEditedMatchToRegister()`).

### Edit match

### Edit match

- **A completed match can be edited, not only undone** (raised 2026-10-19: several matches were reported incompletely, and achievements, legs won and winners all had to be corrected; achievements entered by hand were tied to no match). **Edit match** sits beside **Undo match** in the bracket's selection bar and opens the Confirm Winner dialog filled in with the recorded result ("Edit: A beats B", **Save changes**). Rules (maintainer): legs won and achievements can always be edited, locked stages included; the winner only when `isMatchUndoable()` allows it (no downstream match played or live, no locked stage), so the winner rule is the undo rule, from one function. Never in a read-only tournament, for walkovers, or for matches reported from the Chalker (QR or network): their leg-by-leg detail in the register would contradict an edit, so those are corrected with undo and replay.
- **Winner unchanged:** the match's `COMPLETE_MATCH` transaction is rewritten in place (additive `editedAt`) and `finalScore` replaced, so history keeps one transaction per match and undo is untouched; the register record gets the new legs and achievements and keeps its `completedAt`. **Winner changed:** the existing undo, then `completeMatch()` for the other player; the transaction carries `editedAt` and `originalWinner`.
- **Achievements are tied to the match:** the dialog's baseline is the players' stats without what the match recorded, so it opens showing "Achievements for this match", and changes become the transaction's new delta. Cancel and Esc restore the stats to how they were when the dialog opened.
- `undoManualTransaction()` now returns the promise of its register deletes (before: nothing), so a winner change writes the new result only after the old record is gone. `rollbackAchievements()` uses a new `subtractAchievementStats()` that the edit shares.
- **Swiss:** the dialog names what locks the winner ("round 2 has started (R2-3 is played) and was drawn from round 1's results"): the next round is drawn from every result of the round, so one played match in it locks them all (the undo rule). Found when the maintainer asked why a round 1 match whose own next matches were unplayed was locked.
- Tested in the browser (8-player double elimination, hand-completed matches with achievements): score and achievements edit, Cancel restoring the stats, Esc, a winner change (the next match and the backside placement follow, one transaction left, `originalWinner` kept, register record updated), the locked winner (note shown, no Change winner button, score still editable, a direct winner change refused), QR, read-only and not-completed matches not editable. Detail in `Docs/EDIT-MATCH.md`.
- Files changed: `js/bracket-rendering.js`, `js/clean-match-progression.js` (`showWinnerConfirmation()`), `js/bracket-view.js`, `js/dynamic-help-system.js`, `Docs/EDIT-MATCH.md`, `Docs/UNDO.md`.

## **v5.4.7** — Urgent PHP Security Release (2026-10-22)

### Release

- Version 5.4.7: `APP_VERSION` and `CHALKER_VERSION`, Chalker cache busters (`chalker.js?v=33`, `chalker-v130`), release pages `releases/index.html` and `releases/v5.4.7.html`, `sitemap.xml`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.4.7.md`. A security release: no application code changed.

### Docker image: PHP security fixes (CVE-2026-109618, CVE-2026-109619)

- **PHP published an out-of-band security release on 2026-10-21** for two critical remote code execution flaws in its core (CVSS 9.2 each), fixed in 8.5.12, 8.4.27, 8.3.36 and **8.2.35**. Remote code execution was confirmed on PHP-FPM, which the Docker image uses. Earlier releases of the image were built on an older 8.2.x.
- **The image is rebuilt on PHP 8.2.35.** `docker/Dockerfile` keeps the floating `php:8.2-fpm-alpine` tag, which carries the fix; a comment says 8.2.35 is the minimum. The publish workflow (`.github/workflows/docker-hub-publish.yml`) now builds with `pull: true`, so the newest base image is taken on every build and a cached layer cannot keep an old PHP.
- **Anyone running the Docker image should pull the new `:latest` and recreate the container** (`docker compose pull && docker compose up -d`). The Chalker app and the single-file tournament page are static and do not run PHP.
- PHP 8.2 is security-supported until the end of 2026; moving the image to PHP 8.4 is planned as a separate release.
- Files changed: `docker/Dockerfile`, `.github/workflows/docker-hub-publish.yml`.

## **v5.4.6** — Peek-a-Boo (2026-10-12)

### Release

- Version 5.4.6: `APP_VERSION` and `CHALKER_VERSION`, Chalker cache busters (`chalker.js?v=32`, `chalker-v129`), release pages `releases/index.html` and `releases/v5.4.6.html`, `sitemap.xml`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.4.6.md`. The help (bracket page) and `llms.txt` (Match Controls) mention the drawer: the strip, the ← Bracket pill and Esc.

### Undo in a completed Swiss, Round Robin or Cup and Plate tournament

- **Undoing a match in a completed tournament left it completed** in Swiss (found by the maintainer: after undoing the final and the bronze final, Match Controls still showed the podium), in Round Robin with one group (any group match), and in Cup and Plate (the Plate's final or bronze final, when one of them was played last). The undo reset the status only for a fixed list of final matches: double elimination's grand final, single elimination's final, and Round Robin's cup finals and bronze finals.
- **Now any undo in a completed Swiss, Round Robin or Cup and Plate tournament takes it back to active** and clears the placings, as the finals did. These formats are over when a check finds everything played, so replaying the match completes the tournament again with fresh placings. Double and single elimination keep their rule: their completion leaves out the bronze final, so undoing it must keep the tournament completed.
- Only when the status is reset changes; how matches are rolled back is untouched.
- Tested in the browser: a completed Swiss (undo the final and the bronze final: active, Match Controls shows them ready; replay: completed, top four placed, the podium back) and a completed one-group double round robin (undo the last match: active; replay: completed).
- Files changed: `js/bracket-rendering.js` (`undoManualTransaction()`).

### Bracket page: Match Controls as a drawer over the bracket

- **First-time users didn't find the bracket** (the maintainer let friends try the Tournament Manager): the page starts on Match Controls, which filled the frame, and nothing said a bracket was behind it; when Match Controls was a dialog, the bracket around it had made that obvious. They also didn't tell the bracket page from the other pages.
- **Match Controls is now a drawer from the right,** over the bracket: a strip of the bracket stays visible on its left, lightly dimmed (16% of the width, 140–260px), and the drawer has a shadow on its left edge. Clicking the strip, or the orange **← Bracket** pill (a thin black border and a thin white ring around it, so it stands out from the dimmed bracket) at the top of the strip beside the drawer's edge, shows the bracket; on hover the pill says "Click or press ←", so the arrow key is discovered too. (A dark pill in the middle of the strip, beside a round ×, was tried first: it blended into the bracket's cards. The accent colour and the top, where the eye comes down from the tabs, make it found at once; one control instead of two.) **Esc closes Match Controls** and shows the bracket as it was left (the bracket's own Esc, which clears the selection, doesn't run then); not while typing, choosing in a list or a dialog is open, nor when the Esc just closed a dialog (the Leaderboard opened over Match Controls closed both at once: the dialogs' Esc handler in `main.js` runs first and marks the key handled, so both Esc handlers on the bracket page now skip a handled key; the bracket's selection no longer clears when Esc closes a dialog either). The Bracket | Match Controls tabs and the arrow keys work as before; the Console still fills the frame.
- **The finished tournament's Leaderboard button in Match Controls is gone** (maintainer): the header's Leaderboard shows above the drawer, so it was a second way to the same place. Tournament Analytics and Export tournament stay.
- **The bracket page stands apart:** its surround is a deeper warm shade (`#e4e0d9`), and the other pages are lighter, near white (`--nt-bg` in `css/design-tokens.css`: `#fbfaf8`, was `#f4f3f1`; also Match Controls' sheet and the Console). The shade alone, against the old page colour, was too subtle to notice (maintainer); a darker bracket page would have cost the header's contrast.
- Tested in the browser: a running 32-player double elimination (round 1 visible in the strip), a finished Cup and Plate's podium (the pill clear of the sheet's heading), before the draw; clicking the strip or the pill shows the bracket; Esc closes Match Controls with the bracket's selection kept, does nothing while a list has focus, and on the bracket still clears the selection.
- Files changed: `css/bracket-view.css`, `css/design-tokens.css`, `tournament.html`, `js/bracket-rendering.js` (`_bvSetView()`: `bv-on-controls`; the Esc handler; `_mcCompletedHTML()`), `js/bracket-view.js` (its Esc skips a handled key), `Docs/BRACKET-REDESIGN.md`.

## **v5.4.5** — Look Sharp! (2026-10-11)

### Release

- Version 5.4.5: `APP_VERSION` and `CHALKER_VERSION`, Chalker cache busters (`chalker.js?v=31`, `chalker-v128`), release pages `releases/index.html` and `releases/v5.4.5.html`, `sitemap.xml`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.4.5.md`; `llms.txt` and the help reviewed (no changes needed beyond this version's own).

### Bracket: loser drops bend halfway

- **A selected match's loser drop bent in the gap just before its target column**, where the backside's own winner lines fork into the next round: the dashed line climbed on top of those forks and was hard to follow (noticed by the maintainer looking for a screenshot). It now bends as near halfway as it can, an S or Z: out horizontally, down or up in the column gap nearest the middle, and horizontally into the player row it fills. A gap qualifies only if the bend crosses no card and runs along no other line; with the finals in the middle that is usually the open space beside the finals.
- Where no gap qualifies, the late bend stays: drops into the backside's outer column (their last leg would cross the next column's cards; their gap has no forks), all drops with the finals on the right, and one drop in 32-player brackets. If the late bend's gap has a fork in it, the bend now sits beside the fork, nearer the target card, instead of on it.
- To know where the forks run, the winner lines are drawn before the loser lines, and the vertical runs of the always-drawn lines are recorded. Only drawing changes: `bracket-view.js` never touches tournament data or the progression tables.
- Tested in the browser: every loser drop in double elimination brackets of 8, 16 and 32 (finals in the middle and on the right) and with qualifiers (40): none crosses a card or runs along a line; 2 of 6, 6 of 14 and 13 of 30 bend near the middle with the finals in the middle. Single elimination, Cup and Plate, Round Robin and Swiss draw as before.
- **A new bracket screenshot** (`Screenshots/tournament-bracket-zoom.png`, by the maintainer) for the landing page's bracket step and the README: a selected match, its loser's drop bending beside the finals. Alt texts say so (it said Follow).
- Files changed: `js/bracket-view.js`, `Docs/BRACKET-REDESIGN.md`, `Screenshots/tournament-bracket-zoom.png`, `landing.html`, `landing-page.php`, `README.md`.

### Docker documentation: port 2020

- **The documented host port was 8080**, but `docker-compose.yml` (and the SSL compose) publish `2020:2020`, so following the quick start led to a page that wasn't there. 2020 is the port by design ("Double 20", double tops; maintainer). Every `8080` in the current docs is now 2020: `http://localhost:2020`, `docker run -p 2020:2020`, the reverse-proxy and localhost-only examples, the troubleshooting steps.
- **`NEWTON_HOST_PORT` removed from the docs:** the quick start's variables table and its "Port already in use" tip used it, but no compose file reads it. The tip now says to change the host port in `docker-compose.yml` (`"9000:2020"`). `docker/README.md`'s own tip still said `8080:80`, from before the internal port was 2020.
- The quick start says "the Tournament Manager is running" instead of "NewTon is running".
- Files changed: `docker-quickstart.html`, `DOCKER-QUICKSTART.md`, `docker/README.md`, `rest-api.html`, `Docs/REST_API.md`, `api/README.md`, `llms.txt`.

### The headline everywhere, and a new share image

- **README and `llms.txt`** led with the old tagline, "The Offline Authority in Darts." Both now lead with the website's headline, "Run the whole darts night. Offline.", and the hero's line under it.
- **The README is rewritten** (maintainer: it had "been suffering from just being accumulated", as the landing page had). It follows the website: the headline, the share image and one line of links (try, download, Docker, guide, release notes, changelog); the three cards (the middle one now "Chalk. Send. Done.", results over your own network or by QR code, instead of "Scan. Score. Done."); How a night runs in seven steps, the bracket third, with the bracket and Match Controls screenshots; Pick your format as a table with each format's line and player range (it said "single and double-elimination" only); the features grouped by the night (before the draw, during, after the final, built to last); Getting started as the website's three ways; Technical details; Credits and the licensing split, unchanged. Gone: the traffic-light colour legend, Use cases, and features described as they were before the bracket redesign (Magic Zoom, click-to-select winners). The Docker snippet now says `http://localhost:2020`, the port `docker-compose.yml` maps (it said 8080).
- **`llms.txt` names the software NewTon DC Tournament Manager** throughout: the About line said "NewTon DC is a web application", and "NewTon" stood for the software in four more places. About now says the club makes it, and Tips for AI Services starts with the naming rule. Its use case mentions network results as well as QR.
- **A new share image**, `Screenshots/newton-share.png` (1200×630): the landing page's hero, condensed (the wordmark, the headline, the new hero bracket and the Chalker on ink with the dartboard, newtondarts.com). It replaces the retro poster as `og:image`, `twitter:image` and the structured data's `screenshot` in both landing files, and as the README's image. Its source is `Docs/mockups/share-image.html`, captured with headless Chrome at 1200×630 (the command is in its head), so it can be redone when the hero changes. The poster files stay.
- Files changed: `README.md` (rewritten), `llms.txt`, `landing.html`, `landing-page.php`, `Screenshots/newton-share.png` (new), `Docs/mockups/share-image.html` (new), `Docs/PARKING-LOT.md`.

### Landing page: a new hero image, not clickable

- **The hero's bracket is a new screenshot**, `Screenshots/hero-se.jpg` (1400 px, by the maintainer): an 8-player single elimination bracket with the final in the middle, a semifinal live. The old one was a crop of a 32-player bracket, too small to read at hero size; the bracket step below now shows the whole double elimination bracket, so the two show different formats.
- **Neither hero image opens the lightbox** any more (maintainer): the hero is a picture of the product, and the timeline right below is the gallery. `lightbox-trigger`, `data-full` and the zoom cursor removed.
- **The Chalker card hangs lower** (the stage's bottom padding 40 → 80 px), so it no longer covers the bracket's lower left quarterfinal; the new image is wider, so the window is shorter.
- Tested in the browser: desktop, 820 px and 375 px (no horizontal scroll; the Chalker card is hidden on phones, as before).
- Files changed: `landing.html`, `landing-page.php`, `css/home.css`, `Screenshots/hero-se.jpg` (new).

### Landing page: the bracket in How a night runs

- **The bracket gets its own step**, step 3 "All night long" (between the draw and Match Controls; the later steps become 4–7): "The bracket, centre stage." The night's timeline went from the draw straight to Match Controls, leaving out what the room watches all night. A featured step, unlike the others: full width with a red number, the text beside four features (Follow a player, click a match, finals in the middle or on the right, every format drawn its own way), and the full bracket screenshot below (`tournament-bracket-zoom.png`, the whole bracket with Follow on; the hero keeps its crop). Requested by the maintainer ("the jewel, what everything runs around").
- Tested in the browser: desktop width and 375 px (no horizontal scroll).
- Files changed: `landing.html`, `landing-page.php`, `css/home.css`.

## **v5.4.4** — Run the whole darts night. Offline. (2026-10-11)

### Release

- Version 5.4.4: `APP_VERSION` and `CHALKER_VERSION`, Chalker cache busters (`chalker.js?v=30`, `chalker-v127`), release pages `releases/index.html` and `releases/v5.4.4.html`, `sitemap.xml`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.4.4.md`; `llms.txt` (the Dashboard's top 10 and Latest tournaments) and the help reviewed; the release template's font preload is Inter (`releases/README.md`). The font preload in the existing versioned release pages was changed with the restyle (Manrope → Inter), the only change to them.

### The format, wherever tournaments are listed

- **Tournament Setup's tournaments list** has a **Format** column (after Date), **the Load tournament dialog** a Format line, and **Analytics' Dashboard** a Format column in Latest tournaments (wide screens, as the Register's). Names as the Register already had them: Double elim., Single elim., Cup & Plate, Round robin, Swiss. A tournament not yet drawn shows "—" in the list and "Not drawn yet" in the dialog: the format is set by the draw, and an absent format would otherwise read as double elimination. Requested by the maintainer.
- **The draw dialog** named Cup and Plate "Single Elimination" (in its title and Format line; only the size, "16 + Plate of 8", told them apart); it now says Cup and Plate.
- **One name per format:** each entry in `TOURNAMENT_FORMATS` has a `short` name, read through `formatShortName(format, plate)` (results-config.js); the Register's Format column and a tournament's facts in Analytics use it instead of their own spelled-out copies. The Server tab of the list is unchanged (the server's list has no format).
- Tested in the browser: the list (Swiss, Round robin, Single elim., Cup & Plate, "—" for undrawn), the Load dialog (Cup & Plate, Not drawn yet), the draw dialog (Cup and Plate with the Plate picked, Single Elimination without), the Dashboard.
- Files changed: `js/results-config.js`, `js/tournament-management.js` (`savedFormatName()`), `js/newton-history.js`, `js/clean-match-progression.js` (`showBracketConfirmation()`), `tournament.html`, `css/setup-page.css`, `js/dynamic-help-system.js`.

### Design System page: the new design; Architecture page: up to date

- **The Design System page** documented the old design (browns, oranges and teal, Manrope, the old landing page's cards). Rebuilt to document the new one, still drawn with the site's own stylesheets (it now loads `css/home.css` as well as `css/landing.css`), so it shows the design as it is: the wordmark on ink with the dartboard; the palette (fourteen colours, each with its role); the type scale (Insignia display, section and card headings; Inter label, lead and body; Cascadia Code); buttons on ink and on paper; a format card, a trust card and the dark Get started card; a step of the night; the documentation components (contents, code, terminal, steps, table); the footer. `css/design-system.css` is rewritten to lay these out only. Swatches use classes, not inline styles (the server's `style-src 'self'`).
- **The Architecture page** still described the app before the formats: a single `MATCH_PROGRESSION` table, "4 to 32" participants, and storage keys that are not the real ones. Now: the two tables (`DE_MATCH_PROGRESSION`, `SE_MATCH_PROGRESSION`); a new "Every format, from the same tables" (qualifiers, Cup and Plate, the cups and Swiss's top four merged in by `getProgressionTable()`, group and Swiss rounds progressing no one); the real keys (`dartsTournaments`, `currentTournament`, `tournament_{id}_history`, `dartsConfig`); the limits (48 players with qualifiers, a 64-slot table needed beyond; Round Robin 3–32, Swiss 4–48). The approach it describes is unchanged.
- Tested in the browser: the Design System page at desktop width and 375 px.
- Files changed: `design-system.html`, `css/design-system.css`, `architecture.html`.

### The documentation and release pages: the landing page's look

- **Every page that shares `css/landing.css`** (user guide, Architecture, Docker quickstart, automatic backup, privacy, REST API, QR protocol, QR generator, licenses, the releases page and all 68 versioned release pages) now has the new landing page's look, with **no change to any page's content or markup**: the stylesheet is rewritten with the pages' own class names.
  - The back link at the top is a dark bar that stays at the top, the full name in Insignia.
  - Headings in Insignia, text in Inter, code in Cascadia Code (all three already in `fonts/`); Contents as a white card with a small red label; links in the dartboard red.
  - Code blocks and terminals on warm ink (the terminal's dots in the board's red, amber and green); tables and the Quick Start steps as white cards; inline code wraps on a phone instead of widening the page.
  - Release pages: the Download button is red, the page's main action.
  - The footer is a dark band, as on the landing page.
- `css/landing.css` loses the old landing page's sections (header, hero, formats, get started, story cards, showcase), now in `css/home.css`; it goes from 1435 to about 670 lines. The QR generator's own rules keep their layout in the new colours.
- The pages preloaded Manrope, which they no longer use; the preload is now Inter (77 pages; `tournament.html` still uses Manrope and is unchanged).
- Tested in the browser: the user guide, Docker quickstart, REST API, the releases page, QR generator and Design System at desktop width; twelve pages (two of them old release pages) at 375 px without sideways scrolling.
- Files changed: `css/landing.css`, the font preload in every page that uses it (`*.html`, `releases/*.html`).

### Landing page: a fresh design

- **A new landing page**, designed from scratch for visitors interested in the software (mockup `Docs/mockups/landing-redesign.html`, approved by the maintainer). Dark, warm and high-contrast, with the dartboard's red and green as the only colours, Insignia for the wordmark and every heading, Inter for text. From the top:
  - **A navigation bar** that stays at the top: the wordmark **NewTon DC Tournament Manager** (NewTon DC is the club; the software has the full name), How it works, Formats, Built to last, **Download**, and a red Launch button.
  - **The hero, in two columns:** "Run the whole darts night. *Offline.*", what it covers, **Launch Tournament Manager** and **Download** (the releases page), No account · No subscription · No cloud, and "Scoring at the board? Open the Chalker". On the right the bracket in a window frame with the Chalker over its corner, in front of a dartboard drawn in CSS (dark segments, a faint red and green doubles ring).
  - **"From the door to the podium.":** the screenshots as a numbered night, in the order it happens: doors open (Player Registration), the draw, on the boards (Match Controls), at the oche (Chalker), the final (Tournament complete), the morning after (Analytics).
  - **Pick Your Format** on dark, the pictograms recoloured for it, the lead line kept.
  - **"Built for the room, not the cloud.":** works offline, yours not theirs, never loses a result (How it's built), free and open source (GitHub).
  - **"Ready when you are.":** Try it now (Launch, the Chalker), Download (the latest release), Self-host (Docker quickstart); then User Guide, How it's built, Release notes, GitHub.
  - **The footer:** the links, "No popups? No cookies!" and the 42.
- Left out: the poster and the tagline "The Offline Authority in Darts" (its message is the headline's "Offline"); the story cards (their points are in the night and in Built to last); the qualifiers note.
- **Its own stylesheet, `css/home.css`:** `css/landing.css` is shared by the documentation and release pages (about 80), which keep it unchanged until they are restyled. No inline styles or scripts (the server's `style-src 'self'`); the lightbox is `js/lightbox.js` as before. The search tags carry over with the full name and the new message (title, description, Open Graph, Twitter, structured data; 3–48 players); the share image is still the poster. `landing-page.php` is the same page with its server parts (redirect, GitHub URL, base URL, canonical tag; the Chalker at `chalker/`).
- Tested in the browser: every local link, image and font loads; the lightbox opens and closes; desktop, 768 px, 390 px and 375 px wide without sideways scrolling.
- Files changed: `landing.html`, `landing-page.php`, `css/home.css` (new), `Docs/mockups/landing-redesign.html` (new), `llms.txt`.

### Landing page: See It in Action in the night's order

- The cards now follow a night: Bracket, Chalker, Player Registration, The Draw, **Match Controls** (moved from third, after the Chalker, to after The Draw), Analytics, Tournament Complete. The maintainer's order.
- Files changed: `landing.html`, `landing-page.php` (the same).

### Swiss's pictogram: the top four's semifinals

- The Swiss pictogram drew the last round's outer two matches straight into the final (the middle two connected to nothing) and a line running out of the final into nothing. Redrawn: three round columns (no lines: the next opponent comes from the table), a gap (the table picks the top four), then two semifinals bracketed into the final, as the finish is played. Spotted by the maintainer on the landing page.
- Files changed: `js/results-config.js` (Swiss's `pic`), `landing.html`, `landing-page.php` (the same copy).

### Landing page: Pick Your Format's lead line

- Under the heading, a lead line sets the feel instead of a note under the cards: **"Small night or full house, there's one for it."** (larger, "full house" in the accent). The note it replaces ("Above 32, qualifiers decide the last places, up to 48. Seeding from your own results keeps the best players apart.") went into detail the cards already give (their player ranges), and the Draw card on the same page already speaks of seeding. The maintainer's choice of the options discussed (a lead under the heading, rather than the line as the heading).
- Files changed: `landing.html`, `landing-page.php` (the same), `css/landing.css` (`.formats-lead` replaces `.formats-note`).

### Landing page: the format pictograms in Pick Your Format

- Each card in **Pick Your Format** now opens with its format's pictogram, the same SVG as on Match Controls' Pick a format cards, on a tinted band, larger (up to 168 px wide). The SVGs are copied as they are from `TOURNAMENT_FORMATS` (`pic`, without the fixed size) and recoloured in `landing.css` by their fills to the page's warm palette, the final in the orange accent; the cards get the page's hover border. Requested by the maintainer: the page looked a little bland, and showing off the formats matters.
- **Swiss's line** is now "Your results pick your opponent." (was "Same record, new opponent.", which said too much in too few words), on the landing page and the Swiss card in Pick a format.
- Files changed: `landing.html`, `landing-page.php` (the same), `css/landing.css`, `js/results-config.js` (Swiss's `line`), `Docs/mockups/swiss.html`, `Docs/PARKING-LOT.md`.

### Analytics: a fuller top 10 on the Dashboard

- **The Leaderboard's top 10** on the Dashboard had Played, 1st and Points only; it now also shows **W–L** (matches, after 1st), **Attendance** and **Achievements** (the points from taking part and from achievements; the rest of Points is placement points). Under the point mode and layers in use, as Points. The three new columns are for wide screens only (`an-wide-only`), as Latest tournaments' Format. Requested by the maintainer ("it looks kinda sparse").
- The split is computed in `_computePlayerRows()` from the same functions as Points (`calculateAchievementPoints()`, `calculatePoints()` without placement), so the parts can't disagree with the total.
- Tested in the browser: attendance 5 + 1st 15 = 20, the layout at 1450 px wide.
- Files changed: `js/newton-history.js`.

## **v5.4.3** — Swiss Precision (2026-10-10)

### Landing page: the draw's new screenshot and copy, and Swiss in Pick Your Format

- **The draw card** in See It in Action, with the maintainer's new screenshots of Pick a format: labelled **The Draw** (was Shuffle & Draw), still "Pick a Format. Draw. Play.", and a text that names no formats, so new ones don't need it changed: "A card for every format, each showing what it means for tonight's players. Pick one and draw. Seeding from your own results keeps the best players apart; otherwise it's a fair random draw." The alt text likewise.
- **Pick Your Format** gets a fifth card, **Swiss** ("Same record, new opponent."), wrapping 3 + 2 as designed; Round Robin's says "played once or twice". The feature list and keywords name Cup and Plate and Swiss.
- Files changed: `landing.html`, `landing-page.php` (the same), `Screenshots/th-tournament-setup.jpg`, `Screenshots/tournament-setup.png` (the maintainer's; committed with `e417384`).

### Match Controls: lanes keep their place

- The lanes board's top line listed only the free lanes, so the row changed as matches started and finished, and a quick click could land on the wrong lane. Now it lists **every lane in use, in order** (labelled Lanes): a free lane starts Next up there, as before; a busy lane stays in its place, dimmed and not clickable, its tooltip naming the match on it ("R2-1 is being played on lane 1"). One helper for both lane boards (`_mcLaneChips()`), the brackets' and Round Robin's. Requested by the maintainer after picking the wrong lane.
- Files changed: `js/bracket-rendering.js`, `css/match-controls.css` (`.mc-lanechip.mc-busy`).

### A new tournament format: Swiss

- **Swiss** (Docs/SWISS.md; mockup `Docs/mockups/swiss.html`): a fixed number of rounds, nobody knocked out. Each round pairs players with the same record who haven't met (Monrad: the table 1st v 2nd, 3rd v 4th, skipping rematches, backtracking when a skip leaves no pairing; the fewest rematches only if none is possible). Round 1 at random, or with seeding ticked the ranked players top half against bottom half. An odd number gives one bye a round (a win, no legs) to the lowest-placed player who hasn't had one. The table: wins, opponents' wins (Buchholz), leg difference, legs won, draw order. 4 to 48 players.
- **Rounds** (by players: 2 for 4, 3 for 5–8, 4 for 9–16, 5 for 17–32, 6 for 33–48; or a fixed number, never more than players − 1) and **Finish** (**Semifinals and final** for the top four, 1st v 4th and 2nd v 3rd with a bronze final, the default; or **The table decides**): chosen on the Swiss card at the draw, starting from Global Settings → Swiss. Placings: the top four from the knockout, everyone else from the table in the shared places. **Match lengths of its own** (Swiss: rounds, semifinal, bronze final, final; 3, 3, 5, 5). Referees: suggestions only.
- **Rounds draw themselves:** the last result of a round draws the next (a `DRAW_SWISS_ROUND` transaction), and after the last round the top four (`DRAW_SWISS_KNOCKOUT`). Undoing a result of the round just finished takes that draw back (its matches and transaction, shown in the undo dialog) as long as none of its matches has started; once one has, the earlier rounds are locked.
- **The three foundations, additions only:** Swiss rounds are in no progression table (like group matches); the top four is `SE_MATCH_PROGRESSION[4]` with its IDs renamed (`K-SF1`, `K-SF2`, `K-B`, `K-F`) returned by `getProgressionTable()`; results are ordinary `COMPLETE_MATCH` transactions.
- **Where it shows:** a Swiss card in Pick a format (a pictogram of rounds into a final; "Tonight: 4 rounds, then the top 4"; matches tonight); Match Controls' queue ("Round 3 of 4", "Top four") and the table in the right column (won–lost, opponents' wins, leg difference, the top four marked, the round's bye); the bracket page with each round as a column of its matches (byes under their round, "Drawn when round 2 is played"), then the top four with lines; a Swiss panel and Swiss match lengths in Global Settings; Analytics names it Swiss.
- **Tested in the browser:** 9 players, 4 rounds and the top four played to the end (each round drawn as the last result came in, no rematches, four different byes, 1st v 4th and 2nd v 3rd, placings 1–4 from the knockout and the rest from the table in shared places); 8 players: undoing a round 1 result took round 2 back, re-entering it drew round 2 again, and starting a round 2 match locked round 1; the Match Controls and bracket views. Found in the maintainer's test: the bracket view only counted itself active for a field size with a double elimination table (8, 16, 32), so 12 players showed "No bracket generated yet"; `isActive()` now accepts Swiss at any size, as Round Robin. The selection bar also named Swiss matches as backside rounds; it uses `Swiss.roundName()`. **Selecting a Swiss match lights up both players' matches across the rounds** (there are no lines between Swiss rounds: the next opponent comes from the table), and the layout keeps room for the top four from the start, so Fit all takes it in before it's drawn.
- Files changed: `js/swiss.js` (new), `js/clean-match-progression.js` (`drawSwiss()`, `drawSwissRound()`, `drawSwissKnockout()`, `swissAfterResult()`, `getProgressionTable()`, completion, rankings, the draw and winner dialogs), `js/bracket-rendering.js` (Pick a format, the queue, the table, undo: `takeBackSwissStage()`, the lock), `js/bracket-view.js` (`layoutSwiss()`), `js/results-config.js` (the format, settings, match lengths), `js/config-page.js`, `js/seeding.js`, `js/main.js`, `js/tournament-management.js` (save, export, load, reset), `js/newton-history.js`, `tournament.html`, `css/match-controls.css`, `js/dynamic-help-system.js`, `userguide.html`, `Docs/SWISS.md`, `Docs/PARKING-LOT.md`.

### Round Robin: with Top half, the fewest groups, odd or even

- The groups were always an even number (the fewest even number that fits Largest group), which makes sense with **Top two**: the group winners and runners-up pair off cleanly in the A cup. With **Top half** (the default) the cups are split from one ranking across the groups, so the even number only cost matches: 12 players drew 4 groups of 3 (12 group matches, 2 each) instead of 3 groups of 4 (18, 3 each), with the same cups of 6 and 6. Now the even number applies with Top two only; with Top half the draw makes the fewest groups that fit (9 players: 3 groups of 3; 10: 3, 3, 4; 20: 5 groups of 4). Tested: 12 players with Top half drew 3 groups of 4, played to the cups (6 and 6).
- Files changed: `js/groups.js` (`groupCount()`), `tournament.html` (Structure and Largest group texts), `js/dynamic-help-system.js`, `Docs/GROUPS-AND-CUPS.md`.

### Round Robin: a double round robin for one group

- **Play each other: Once / Twice** (Round Robin, one group): Twice is a double round robin. The draw makes the one group's schedule twice: the whole first round robin, then the return round in the same order with player 1 and player 2 swapped and the same planned referees. Match numbers continue (4 players: A-1 to A-6, then A-7 to A-12), the return matches are marked (`returnRound`) and named "Round robin · return round", and Match Controls' queue says "Return round". Both meetings count in the table, head-to-head included; the table decides the placings, as in one group.
- **At the draw** it's an option on the Round Robin card when Structure is One group ("Tonight: One group of 4, twice"; matches tonight and per player doubled), for that draw only; **Global Settings → Round Robin → Play each other** decides how it starts (shown for one group; **Once** by default, `config.roundRobin.meetings`, absent = 1). 3 to 8 players, as one group. The tournament keeps it (`groups.settings.meetings`) and its format reads "Round robin, one group, twice".
- **Tested in the browser:** 4 players, Twice at the draw (Global Settings still Once): 12 matches, A-7 the return of A-1 with the players swapped, played to the end with all 4 placed.
- Files changed: `js/groups.js` (`meetings` in the settings and the draw choice, the round and format names), `js/clean-match-progression.js` (`drawGroups()`), `js/bracket-rendering.js` (the Round Robin card's option and estimates, the queue's Return round), `js/results-config.js`, `js/config-page.js`, `tournament.html`, `js/dynamic-help-system.js`, `userguide.html`, `Docs/GROUPS-AND-CUPS.md`, `Docs/PARKING-LOT.md`.

### Match Controls before the draw: pick a format, then draw

- **Pick a format** replaces Shuffle & Draw's stack of buttons and moves into the main column, under Players: a card for every format (Double Elimination, Single Elimination, Cup and Plate, Round Robin), each with its line and what it means for tonight's paid players ("16-player bracket · 4 byes", "Cup of 16 · Plate of 8", "4 groups of 3"). The picked card opens its description, a few bests, three facts (player range, **roughly how many matches tonight**, matches per player) and its options; **one Draw button** draws it ("Draw Cup and Plate →"), with a summary beside it. It opens on the **format of the last tournament drawn in this browser** (if it fits tonight), else the first offered one that fits.
- **Formats not offered** (Global Settings → Formats to offer) are still shown, greyed out and locked: "Not offered: Global Settings → Formats to offer". A format that doesn't fit tonight's field can still be picked (its options may make it fit) and says why on the card and the Draw button.
- **Cup and Plate is its own format**: a card at the draw and an entry in Formats to offer, drawn as single elimination with a Plate (`TOURNAMENT_FORMATS` entry `CP`: `draw: 'SE'`, `plate: true`). The Play a Plate switch on single elimination's card and the Offer Play a Plate setting are gone; a config saved with Offer Play a Plate off starts with Cup and Plate unticked (`loadConfiguration()`). Setup and the finished view name it Cup and Plate.
- **Round Robin's Structure and To the A cup can be chosen at the draw**, for that draw only (`Groups.setDrawChoice()`); Global Settings decide how they start and stay as they are. The tournament records what it was drawn with, as before.
- **Settings for this tournament follow the picked format** (its match lengths: double or single elimination's, the Plate's, Round Robin's group and cup lengths); it always showed double elimination's. **Seeding** applies to every format and its panel follows the picked format; with seeding off in Global Settings, the panel says the draw is random.
- **A pictogram on each card**, in the style of Global Settings' Finals position pictures (grey lines and boxes, the final dark; 96×44): Double Elimination as the real bracket with the finals in the middle (frontside left into the grand final, backside right, lighter, into the backside final below it), Single Elimination as a plain tree, Cup and Plate as the Cup and a lighter half-size Plate back to back with the finals facing, Round Robin as two group tables feeding an A and a B cup, each a small knockout. The picked card's final turns orange; a locked card fades it.
- The formats list (`TOURNAMENT_FORMATS`) carries each card's text (`line`, `about`, `bests`) and its pictogram (`pic`), so a new format brings its own description and picture. Mockup: `Docs/mockups/match-controls-setup.html`.
- **Tested in the browser:** 12 players; the default pick (the last tournament's format); Round Robin switched to one group (the card and the button say "At most 8 players in one group") and back; Round Robin drawn with Top two for this draw while Global Settings keep Top half; a saved config with Offer Play a Plate off (Cup and Plate locked, with the reason); drawing Cup and Plate (a Plate of 8) and then plain single elimination (no Plate).
- Files changed: `js/bracket-rendering.js` (`_mcSetupHTML()` and its helpers, the finished view's format name), `js/results-config.js` (`TOURNAMENT_FORMATS`, the Offer Play a Plate fold-in), `js/groups.js` (`setDrawChoice()`), `js/plate.js` (`offered()`), `js/seeding.js` (`html(formatId)`), `js/tournament-management.js` (Setup's format name), `tournament.html`, `css/match-controls.css`, `js/dynamic-help-system.js`, `userguide.html`, `Docs/CUP-AND-PLATE.md`, `Docs/mockups/match-controls-setup.html`.

### GitHub Actions: off Node.js 20

- Both Docker workflows used actions targeting Node.js 20, which GitHub has deprecated (each build warned that they were forced onto Node.js 24). Moved to the current majors, all on Node.js 24: `actions/checkout@v7`, `docker/setup-qemu-action@v4`, `docker/setup-buildx-action@v4`, `docker/login-action@v4`, `docker/metadata-action@v6`, `docker/build-push-action@v7`. The workflows only use inputs that are unchanged in these versions (the removed ones were deprecated inputs and environment variables they never used). Build Push v6+ adds a build summary to each run's page and a build record file to the run.
- **The image digest is printed again:** the GitHub Container Registry workflow's last step reads the digest from a step called `build`, which didn't exist, so it always printed empty; the build step now has `id: build`.
- Files changed: `.github/workflows/docker-build.yml`, `.github/workflows/docker-hub-publish.yml`, `Docs/PARKING-LOT.md`.

### Release

- Version 5.4.3, "Swiss Precision": Swiss, Pick a format with pictograms (Cup and Plate as its own format), the double round robin, Round Robin's fewer groups with Top half, the lanes row keeping its place, and the landing page. The first release built with the GitHub Actions on Node.js 24 (to confirm on this release's Docker build). The Chalker is unchanged; its version and caches are bumped with the app's (`CHALKER_VERSION`, `chalker.js?v=29`, service worker cache `chalker-v126`).
- Files changed: `js/main.js` (`APP_VERSION` → `5.4.3`), `chalker/js/chalker.js` (`CHALKER_VERSION` → `5.4.3`), `chalker/index.html`, `chalker/sw.js`, `llms.txt` (Swiss, Pick a format, the double round robin, Cup and Plate's own card, the odd group count; Swiss off the do-not-invent list, the parked formats on it), `sitemap.xml`, `releases/index.html`, `releases/v5.4.3.html`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.4.3.md`. The help was reviewed with each change.

## **v5.4.2** — All the Dishes Rattle in the Cupboards (2026-10-09)

### Landing page: Pick Your Format, and Chalk. Send. Done.

- **"Pick Your Format."**, a new section between the hero's Launch/Open buttons and Get Started: a card for each format (Double Elimination, Single Elimination, Cup and Plate, Round Robin) with a short line and its player range, and a note on qualifiers and seeding. The cards sit in centred rows that wrap, so new formats fit as they arrive: up to 4 across; 5 or 6 go 3 to a row, 7 or more 4 to a row again (`:has()`; older browsers wrap 4 + 1, still centred); 2 + 2 on a tablet, one per row on a phone.
- **"Chalk. Send. Done."** replaces "Scan. Score. Done.": matches go out and results come back by QR code or over your own network.
- Files changed: `landing.html`, `landing-page.php` (the same hero and section), `css/landing.css` (`.formats`, `.format-cards`, `.format-card`).

### Reset: never a completed tournament, and an unfinished one leaves the Analytics register clean

- **A completed tournament can't be reset** (maintainer, 2026-10-09). Its results are final, and Reset threw them all away; the link is gone from Tournament Setup for a completed tournament, and `showResetTournamentModal()` and `confirmReset()` refuse one anyway. To correct a result after completion: Developer Console → Toggle Read-Only, then undo the match.
- **Reset clears an unfinished tournament's Analytics record.** The register keeps a hidden "live" record of every match as it is played, and Reset left it alone, so after a reset and a new draw, a match that became a walkover (never recorded) kept its old result: two matches from an earlier draw, with players no longer in the tournament, showed up in Analytics when the tournament completed. Reset now deletes the tournament's record and matches from the register (`NewtonDB.deleteTournament()`) when the record isn't final; a final record is never touched. Undo already cleaned up after itself; Reset didn't, since the register was added.
- Files changed: `js/tournament-management.js` (`renderSetupCurrent()`, `showResetTournamentModal()`, `confirmReset()`), `js/dynamic-help-system.js`.

### Cup and Plate in Analytics: "Cup & Plate"

- Analytics named a Cup and Plate night "Single elim.", since the register only stores the format id (`SE`). The tournament record now also keeps `plate` (when finalized, and when imported into the register), and Analytics says **Cup & Plate** in the tournament list, the tournament's facts and the import dialog. Plate matches were always recorded like any other (their achievements, matches and legs count); walkovers, as in every format, are not.
- Files changed: `js/clean-match-progression.js` (finalize), `js/newton-db.js` (`backfillTournament()`), `js/newton-history.js`.

### Cup and Plate: single elimination where the round 1 losers play a second knockout

- **Play a Plate**, a switch on single elimination's card in Shuffle & Draw (from 5 players: a Cup of 8 or more). It always starts off; **Offer Play a Plate** in Global Settings (on by default, `config.ui.offerPlate`) leaves it out of the draw for clubs that never play one. Design and decisions: `Docs/CUP-AND-PLATE.md`; mockup `Docs/mockups/cup-and-plate.html`.
- **The Plate** is a single elimination of half the Cup's size (8 → 4, 16 → 8, 32 → 16) with its own bronze final, for the Cup's round 1 losers at fixed places, as round 1 losers drop to the backside: the losers of round 1 match 1 and 2 meet in P-QF1 (or P-SF1, P-R1-1), and so on, so a Plate match can start as soon as its two round 1 matches are played. A bye's Plate place is a walkover (auto-advanced as on the backside); a qualifier loser is not qualified and never goes to the Plate. Match lengths as single elimination's, by the Plate's own rounds. Bronze finals are always played, in the Cup and the Plate (a switch was considered and dropped: it would change the points and touch how every single elimination tournament ends).
- **The three foundations, additions only:** `getProgressionTable()` merges the Plate in after any qualifiers: each Cup round 1 entry gains a loser path into the Plate, and the Plate's own table is `SE_MATCH_PROGRESSION[size]` with its IDs renamed by `cupMatchId('P', …)`. The tables themselves are unchanged; advancing, walkovers, undo (a round 1 result can be undone until its Plate match starts) and the bracket's "Loser FS-1-3" work as they are.
- **The end:** with a Plate the tournament completes when every match is played (the Cup's final and bronze final and the Plate's), whichever comes last; 1st and 2nd come from the Cup's final. **Placings:** the Plate orders the round 1 losers in the shared places, like the B cup (8 players: its finalists 5th–6th, its bronze pair 7th–8th; 16: its semifinalists 9th–12th, its first round's losers 13th–16th); its final and bronze final decide the Plate, and the finished view names the **Plate winner**.
- **Bracket view: back to back** (`layoutPlate()`): the Cup as single elimination with the finals on the right, the Plate mirrored from the right, each of its rounds level with the Cup's next round, so the two finals face each other in the middle, with the club name between them and nothing joining them. "Cup ▶" and "◀ Plate", the Plate shaded like the backside, its bronze final tied to its final as the Cup's is. Round 1 losers' lines run round under the bracket into the Plate (on selection). Always back to back with a Plate; the Finals toggle is hidden. Qualifiers stay outside the Cup's round 1.
- **Match Controls:** "Cup · Round 1", "Plate · Quarterfinal" in one queue, each Plate round after the Cup's round of the same number. Winner dialog and match history: "moves to P-QF1", "wins the Plate". The draw dialog says "16 + Plate of 8".
- **Tested in the browser:** 16 players to the end (the Cup final played with 8 Plate matches left, and the tournament waited for the Plate; 1st and 2nd from the Cup; placings 1–4, 5th–8th, 9th–12th, 13th–16th; Plate winner on the finished view); 12 players (4 byes, their Plate places walkovers); undo of a round 1 result (its loser out of the Plate again); 8 players (a Plate of 4); 4 players (no Plate even with the switch on); 40 players with qualifiers and a Plate of 16; save and reload; the switch on the draw card.
- Files changed: `js/plate.js` (new), `js/clean-match-progression.js` (`getProgressionTable()`, the draw, completion, `calculateAllRankings()`, the draw dialog, the winner dialog), `js/bracket-view.js` (`layoutPlate()`, labels, lines, the finals toggle), `js/bracket-rendering.js` (the switch, queue, round names and order, the Plate winner), `js/groups.js` (`placeTier` exported), `js/main.js`, `js/tournament-management.js` (save, export, load, reset), `js/results-config.js` (`offerPlate`), `tournament.html`, `css/match-controls.css`, `js/dynamic-help-system.js`, `userguide.html`, `Docs/CUP-AND-PLATE.md`, `Docs/PARKING-LOT.md`.

### Release

- Version 5.4.2, "All the Dishes Rattle in the Cupboards": Cup and Plate, Reset only for unfinished tournaments (clearing their Analytics record), Cup & Plate in Analytics, and the landing page's formats section. The Chalker is unchanged; its version and caches are bumped with the app's (`CHALKER_VERSION`, `chalker.js?v=28`, service worker cache `chalker-v125`).
- Files changed: `js/main.js` (`APP_VERSION` → `5.4.2`), `chalker/js/chalker.js` (`CHALKER_VERSION` → `5.4.2`), `chalker/index.html`, `chalker/sw.js`, `llms.txt` (Cup and Plate; off the do-not-invent list, with optional bronze finals on it; a completed tournament can't be reset), `sitemap.xml`, `releases/index.html`, `releases/v5.4.2.html`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.4.2.md`. The help was reviewed with the Cup and Plate work.

## **v5.4.1** — Under Pressure (2026-10-09)

### Round Robin: without a B cup, the A cup takes the top two of each group

- Top half is there to keep the B cup from being tiny, so it now only applies when the B cup is played. With **Play the B cup** off at Draw the cups, the A cup takes the group winners and runners-up whatever To the A cup says, and the rest are placed after it by their group results. Before, Top half without a B cup shrank the A cup to half the field (12 players in 4 groups of 3: an A cup of 6 with 2 byes, instead of all 8 winners and runners-up; the maintainer: no top-two player should miss the knockout when there is only one cup).
- Draw the cups shows it as the switch is flipped (the A field changes, and the note says why), and the draw uses the same fields: `Groups.cupFields(playB)`, called with the switch by `drawCups()` and `_mcDrawCupsHTML()`. Tested: 12 players, Top half: 6/6 with the B cup, 8 and 4 (not played) without; drawn and played to the end, 9th–12th for the thirds.
- Files changed: `js/groups.js` (`cupFields()`), `js/clean-match-progression.js` (`drawCups()`), `js/bracket-rendering.js` (`_mcDrawCupsHTML()`), `tournament.html` (To the A cup), `js/dynamic-help-system.js`, `Docs/GROUPS-AND-CUPS.md`.

### Tournament Setup: the next step's two buttons are the same size

- The dark main button in the Next step bar was 40px with 15px text, the button beside it the standard 36px with 14px, so the pair didn't line up. Both are now 40px with 15px text; the dark colour alone marks the main step (also in the Completed state, where neither is dark).
- Files changed: `css/setup-page.css` (`.st-next .st-btn`).

### Match Controls: a live match without a lane has a plain amber border

- A live tile without a lane had a grey border with a thick amber stripe inside its top edge, which curved into a "lip" at the rounded corners. The stripe is gone; the tile has a 2px amber border all round (the border plus a 1px inset shadow, so nothing inside moves), and the orange "No lane" label carries the warning.
- Files changed: `css/match-controls.css` (`.mc-lane.mc-nolane`).

### Qualifiers: double and single elimination for 33 to 48 players

- **Up to 48 players** in double and single elimination. The bracket stays a 32-player bracket; the players above 32 make it in through **qualifiers**, a round 0 before round 1: one qualifier for each player above 32 (40 players: 8 qualifiers, 24 straight in; 48: 16, one in every round 1 match). Requested by other users of the software. Design and decisions: `Docs/QUALIFIERS.md`; mockup `Docs/mockups/qualifiers.html`. 64 players is a possible future option.
- **The draw** reuses the bye layout as it is: the straight-in players are drawn into 32 places by `createOptimizedBracketV2()` (random or seeded), which leaves one gap per round 1 match where byes would go (opposite the best seeds first), and each gap becomes the place of a qualifier's winner. Who plays a qualifier: drawn at random, never a seed; with seeding **All**, the lowest-ranked (the unranked first, then up from the bottom). Qualifiers are numbered Q1… from the top of the bracket, numbered before the bracket's matches, and played at the format's regular rounds length.
- **The three foundations, additions only:** `getProgressionTable()` merges the qualifier entries (Qn → its round 1 place, no loser path) in front of the 32-player table, which is unchanged, so advancing, undo (a qualifier can be undone until its round 1 match starts) and the bracket's "Winner Qn" work as they are. A qualifier result is an ordinary `COMPLETE_MATCH`.
- **A qualifier only decides who gets to play:** the loser is not qualified, placed shared **33rd–48th** and marked (`tournament.notQualified`); nothing in a qualifier counts, for the winner either: score only in the winner dialog and from the Chalker (no achievements, no raw legs), never in the register, and the tournament's leg counts skip it. The tournament result still goes to Analytics, so a non-qualified player counts as attending.
- **Also for qualifier losers** (Global Settings → Points, under Taking part; on by default): whether a player who loses a qualifier gets the Taking part points. `calculatePoints()` (the one points formula) takes it into account; Analytics reads `notQualified` from the register, and Custom point mode applies the current setting to earlier tournaments too.
- **Bracket view:** a Qualifiers column one step outward from round 1, each qualifier level with the match it feeds (both outer edges with single elimination's finals in the middle). **Double elimination with qualifiers is always drawn with the finals in the middle**: with them on the right the backside sits straight beside round 1, where the qualifiers go; the Finals toggle shows it.
- **Match Controls:** Qualifiers first in the queue (each unblocks a round 1 match) and in Next up; qualifier losers show in the referee list's losers, free for referee duty; Shuffle & Draw says "Draw 32 + 8 qualifiers"; the add-player row stays until 48. Player Registration says how many qualifiers decide the last places. Match history says "not qualified (33rd)".
- **Tested in the browser:** 40-player double elimination and 44- and 48-player single elimination, both finals positions; who plays the qualifiers in each seeding mode; undo (blocked once the round 1 match is live, "Winner Q1" back after); a whole 40-player double elimination played to the end (placings 1st to 33rd, the register final with 40 attending, 8 not qualified and no qualifier matches); Taking part on and off; save and reload.
- **The web pages say 48:** the landing pages (keywords, the feature list, which now also names Round Robin and Qualifiers, and the Shuffle & Draw card: "up to 48 with qualifiers"), the `tournament.html` description, README (a Qualifiers line; live rankings to 48th) and `llms.txt` (DE and SE 4–48, a Qualifiers line, Largest group in the Round Robin line, and "more than 48 players" in place of "more than 32 players or qualifiers" on the do-not-invent list). The in-app help and the user guide already did.
- Files changed: `js/qualifiers.js` (new), `js/clean-match-progression.js` (`getProgressionTable()`, `calculateBracketSize()`, the draw, the confirmation dialog, `calculateAllRankings()`, the register skip, the winner dialog), `js/bracket-view.js` (the Qualifiers column, the finals rule, the toggle), `js/bracket-rendering.js` (queue, round names, Shuffle & Draw), `js/results-config.js` (limits, 33rd, the setting, `calculatePoints()`, leg counts), `js/newton-history.js`, `js/newton-db.js`, `js/qr-bridge.js`, `js/main.js`, `js/player-management.js`, `js/tournament-management.js` (save, export, load, reset), `tournament.html`, `css/styles.css`, `js/dynamic-help-system.js`, `userguide.html`, `landing.html`, `landing-page.php`, `README.md`, `llms.txt`, `Docs/QUALIFIERS.md`, `Docs/PARKING-LOT.md`.

### Round Robin: Largest group and the cups' own match lengths

- **Largest group** (Global Settings → Round Robin): 4 (the default, as before), 5 or 6. The draw makes the smallest even number of groups that keeps every group at that size or less, so a larger limit means fewer, longer groups: 10 players are 3, 3, 2, 2 at 4, and two groups of 5 at 5 or 6 (no more A cup of 8 and B cup of 2 with top two); 20 players are four groups of 5. Groups of 5 and 6 use the circle-method schedule One group already had (in a group of 5 the player sitting the round out referees). The cup fields now take every group place, not just 1st–4th (`cupFields()`), so 5th and 6th go to the B cup with top two, or into the ranking with top half; Draw the cups names them "5th", "6th". At 6, top two with 25 or more players can make a B cup of more than 16, drawn in the 32-player single elimination table (tested: 30 players, A cup 12 in 16, B cup 18 in 32). Each tournament keeps the largest group it was drawn with (`tournament.groups.settings.maxGroup`; absent = 4).
- **The cups get their own match lengths** (Global Settings → Match length → Groups and cups): **Cup rounds** (every round before the semifinals), **Cup semifinal**, **Cup bronze final** and **Cup final**, the same for the A and the B cup, taken when the cups are drawn. Defaults 3, 3, 5, 5, as single elimination. The cups used the single elimination lengths before, so a saved config without the new fields keeps them (cup rounds from the SE quarterfinal, the rest from the SE semifinal, bronze final and final); a normal single elimination night and the cups can now differ (e.g. a Bo7 cup final).
- Both are in the Reset all config preview, with their Global Settings names.
- Files changed: `js/results-config.js` (defaults, the old-config fallback, form load/save, Reset to defaults, `CONFIG_LABELS`), `js/clean-match-progression.js` (`drawCups()` lengths; `roundRobinSchedule()` comment), `js/groups.js` (`maxGroup` in the settings and snapshot, `groupCount()`, `cupFields()`), `js/bracket-rendering.js` (place names at Draw the cups), `tournament.html` (Largest group, four cup steppers), `js/dynamic-help-system.js`, `userguide.html`, `Docs/GROUPS-AND-CUPS.md`, `Docs/PARKING-LOT.md`.

### Release

- Version 5.4.1, "Under Pressure": qualifiers for 33 to 48 players in double and single elimination, and three Round Robin improvements (Largest group, the cups' own match lengths, the top two of each group without a B cup). The Chalker is unchanged; its version and caches are bumped with the app's (`CHALKER_VERSION`, `chalker.js?v=27`, service worker cache `chalker-v124`).
- Files changed: `js/main.js` (`APP_VERSION` → `5.4.1`), `chalker/js/chalker.js` (`CHALKER_VERSION` → `5.4.1`), `chalker/index.html`, `chalker/sw.js`, `sitemap.xml`, `releases/index.html`, `releases/v5.4.1.html`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.4.1.md`. `llms.txt` (reviewed: DE/SE 4–48, Qualifiers and Largest group came with the qualifier work; now also the A cup's top two without a B cup and the cups' own match lengths), and the help (reviewed with the same work).

## **v5.4.0** — Just Around the Corner (2026-10-08)

### The bracket always shows the current tournament

- **The old tournament's groups stayed on the bracket page.** With a Round Robin in its group stage, creating a new tournament and opening the bracket showed the old tournament's group cards. The cards are drawn in their own panel, which `clearBracket()` doesn't reach, and showing the Bracket tab didn't redraw it. Creating, resetting, loading or importing a tournament without a draw never redrew the bracket. Now the bracket is redrawn whenever the Bracket tab shows (`renderBracket()` in `showBracketView()`), so it always shows the current tournament, by whatever path it became current. The same tournament redrawn keeps its zoom, selection and Follow.
- Files changed: `js/bracket-rendering.js` (`showBracketView()`).

### Landing page: new screenshots and copy

- **See It in Action has new screenshots and new copy throughout**, matching the app as it is now: Bracket (every round on one page, the three formats), Chalker App (**Scan It or Send It.**: QR and network handover side by side), Match Controls, Player Registration (who's here, who's paid; the draw waits for everyone), **Shuffle & Draw** (formats, seeding and fair byes; it takes over the Fair Draw card's message), **Analytics** (new; replaces Fair Draw), and Tournament Complete (the podium for the club chat).
- **Lighter thumbnails:** 1000px JPGs, about 430 KB together instead of 1.3 MB; the full-size images stay PNG for the lightbox.
- **Old screenshots removed** (the earlier gallery's images and some unused ones); the poster artwork moves to `Screenshots/posters/`; `backup.png` stays (Automatic Backup uses it). The commented-out Quick Start image is gone from both landing pages.
- Files changed: `landing.html`, `landing-page.php` (the same gallery for the Docker landing page), `Screenshots/`.

### Bracket page header: a frame at both ends, tools in line

- The tournament name and date get the clock's divider, mirrored (a line to their right), so the header is framed at both ends.
- The bracket tools (Finals, Fit all, zoom) were pinned to the top of the header and are smaller than the view buttons, so their tops lined up but their centres didn't. They now get the top row's height and are centred in it, on the same line as Bracket, Match Controls and Leaderboard.
- Files changed: `css/bracket-view.css`.

### Match Controls keeps the page header up to date

- The header's counts (players, live, ready) were only filled in when the bracket was drawn, so adding, removing or marking players paid in Match Controls, or starting and finishing matches there, left them behind. Match Controls now refreshes the header each time it redraws (`BracketView.updateHeader()` in `showCommandCenterModal()`).
- Files changed: `js/bracket-rendering.js`.

### Match Controls: the free lanes and Next up come first

- The Free line (the free lanes and Next up) moved from under the live matches to straight under the Lanes heading, in every format. It used to move down the page as matches started; now it stays in the same place all night, and starting the next match is the first thing on the page.
- Files changed: `js/bracket-rendering.js` (the lanes panel in `_mcActiveHTML()` and `_mcGroupsHTML()`), `css/match-controls.css` (`.mc-free`: the divider below it).

### Round Robin: every group keeps its place in Up next

- A group with nothing left to start used to disappear from Up next, and a column whose groups were all finished only said "These groups are done.", without saying which. Now every group keeps its heading: a finished group says "All 3 played", in a lighter grey, with nothing under it; a group whose last matches are live keeps the normal colour (it isn't done) and says "2 of 3 played · 1 live". Any group with a live match says so in its heading. The groups stay in place all night, and what's done can be read at a glance (the results stay in the group tables on the right).
- Files changed: `js/bracket-rendering.js` (`_mcGroupsHTML()`), `css/match-controls.css` (`.mc-qround.mc-qdone`).

### Round Robin: level only when the group is finished

- Players were marked **level** (with ▲ to decide) while their group still had matches to play, often two players who hadn't met yet, whose match would settle it. Now `Groups.standings()` marks players level only once the group has played every match, so the tables on both pages, ▲ and the warning at Draw the cups follow from one place. After a finished group, two players can't be level (their match decides); only a three-way cycle with identical legs can, and ▲ appears for it at Draw the cups. With one group the tournament completes on the last match, so the group seed decides such a tie.
- Files changed: `js/groups.js` (`standings()`), `js/dynamic-help-system.js`, `Docs/GROUPS-AND-CUPS.md`, `Docs/PARKING-LOT.md`.

### New defaults in Global Settings

- **Round Robin:** Groups and cups, **Top half** to the A cup, Allow group rematches, Play the B cup (Top half is the new one; the A and B cups come out the same size).
- **Seeding: Available** (was Off): Shuffle & Draw offers seeding, unticked, so nothing changes unless the operator ticks it.
- **Chalker handover: None** (was QR code) for new installs and Reset to defaults. A config saved before the Handover setting existed has always meant QR code, so `loadConfiguration()` keeps it QR code (additive-only schema: an absent field keeps its old meaning); a config that has it keeps its choice.
- Settings already saved are unchanged; the defaults apply to new installs, Reset to defaults, and settings a saved config doesn't have yet (Round Robin, which is new; Seeding for a config from before v5.3.1).
- Files changed: `js/results-config.js` (`DEFAULT_CONFIG`, `loadConfiguration()`, the Round Robin apply/save fallbacks), `js/groups.js` (`configSettings()`), `js/dynamic-help-system.js`, `js/types.js`, `Docs/GROUPS-AND-CUPS.md`.

### Reset all config: brings the settings back, keeps the server connection

- **The server connection survives a reset.** Reset all config (Developer Console) and Reset to defaults used to set everything to `DEFAULT_CONFIG`, the server section too: the Analytics server's address and API key were wiped, and a new server ID was made on reload (so the Chalker's QR codes carried a new identity). Both now use `defaultConfigKeepingConnection()`, which keeps `remoteUrl`, `remoteApiKey`, `remoteVerified` and `serverId`; they can still be changed by hand in Global Settings. The reset is a way to bring the settings back to sensible values, not a factory reset.
- **The preview lists every setting that would change.** It was a hand-written list that had fallen behind (no single elimination or group match lengths, Game, Short leg, Require a lane, Hidden formats, New players are paid, Finals position, Chalker, Seeding, Round Robin, or server settings). `configChangesOnReset()` now walks `DEFAULT_CONFIG`, so a new setting is always included, and shows only what changes, by section, with the Global Settings names (`CONFIG_LABELS`; a setting without a name shows its key). Nothing to change says so.
- Files changed: `js/results-config.js` (`SERVER_CONNECTION_KEYS`, `defaultConfigKeepingConnection()`, `CONFIG_LABELS`, `configChangesOnReset()`, `resetConfigToDefaults()`), `js/analytics.js` (`commandResetAllConfig()`, `executeResetAllConfig()`).

### Round Robin: one group, Top half, and its own settings

- **The format is called Round Robin**, with its own panel in Global Settings → Tournaments while it is offered (review note 9): **Structure**, Groups and cups (6–32 players) or **One group** (3–8 players: everybody plays everybody, the table decides the placings, no cups); **To the A cup**, Top two of each group or **Top half** (everyone ranked across the groups and split into two cups of the same size, so 10 players play two cups of five instead of eight and two; note 2); **Play the B cup** on or off to start with. A tournament keeps the structure and cup entry it was drawn with (`tournament.groups.settings`, additive).
- **One group:** a fixed order for five to eight players from `roundRobinSchedule()` beside the group tables (circle method; every pair once; referee from the group, never twice in a row, duties even or within one), planned referees and waits as in the groups, placings from the table when the last match is played. Match Controls shows the next six matches and the table; the bracket page shows the one group (no Cups).
- **Group rematches in cup round 1** (a setting, from last night's list): **Allow** (the default, the mirror draw as it falls) or **Avoid**: each seed gets the nearest opponent from another group, as close to the mirror as possible (`Groups.avoidRematches()`); the Draw the cups step says so. Tested: 16 players, 2 rematches per draw with Allow, none with Avoid; 32 players none.
- **Play the B cup is a switch** in the Draw the cups bar, as in the mockup (note 3).
- The Shuffle & Draw card, the draw dialog, the Setup facts and Analytics follow the structure ("Draw one group of 6", "Round robin, one group").
- Files changed: `js/clean-match-progression.js` (`roundRobinSchedule()`, the limits in `generateCleanBracket()`, the dialog), `js/groups.js` (`configSettings()`, `settings()`, `isSingle()`, `limits()`, `formatName()`, one group in the draw, completion and placings, Top half in `cupFields()`), `js/results-config.js` (`roundRobin` defaults, apply/save, the registry name), `js/config-page.js`, `tournament.html`, `js/bracket-rendering.js`, `js/bracket-view.js`, `js/tournament-management.js`, `js/newton-history.js`, `css/match-controls.css`, `css/bracket-view.css`, `js/dynamic-help-system.js`, `Docs/GROUPS-AND-CUPS.md`.

### Groups and Cups: after the first review

- **Referees are a plan until the match is live** (review notes 1, 6): in this format a referee chosen for a match that hasn't started no longer blocks other matches, so swapping referees between two group matches can't lock them both. Start checks that a referee chosen beforehand is free.
- **Matches wait for their referee** (note 4): Match Controls holds a ready match while one of its players referees an earlier match in the group or cup first ("Ken referees A-QF1 first"), or while its planned referee isn't known yet ("its referee is the loser of A-QF1"), with Start disabled; choosing another referee releases it. The bracket and the group cards show it as Waits/Waiting. Next up only suggests matches that can start.
- **Plans never point at a walkover** (note 7): a two-player B cup's final has no planned referee (so it is Ready), and its empty bronze final reads "Not played".
- **Everyone is placed** (note 8): after the A and B cups' finals and bronze finals, the A cup's other losers (latest round first), then the B cup's, each round by group results, then anyone in no cup, on shared places (9th–12th, 13th–16th …). A B cup too small to fill 7th–8th leaves those places to the best A cup quarterfinal losers. Cup losers are now struck through in the bracket, like eliminated players in the other formats.
- **The Ready label and Bo chip on bracket cards were cut off** ("READ", in every format) since the Groups and Cups build: the group card's match row class `.bv-grow` had the same name as the card's spacer. Renamed to `.bv-gmatch`. The header's ready count leaves out matches that wait.
- Files changed: `js/groups.js` (`holdFor()`, `realCounts()`, `planCupReferees()`, `placements()`), `css/bracket-view.css`, `js/bracket-rendering.js` (live-only referee conflicts for this format, Match Controls rows), `js/clean-match-progression.js` (start check), `js/bracket-view.js`, `css/match-controls.css`, `js/dynamic-help-system.js`, `Docs/GROUPS-AND-CUPS.md`.

### A new tournament format: Round Robin (groups and cups)

- **Groups and Cups**, a third card under Shuffle & Draw (it can be hidden in Global Settings like the others): everybody plays everybody in groups, then an A cup and a B cup. Built for the weekly nights (15–32 players) and the December season final. Design: `Docs/GROUPS-AND-CUPS.md`; mockup: `Docs/mockups/group-night.html`.
- **The groups:** the smallest even number of groups that keeps every group at four or fewer (6–8 players: 2 groups, 9–16: 4, 17–24: 6, 25–32: 8). Players go in in snake order, by ranking when seeding is on (every ranked player; the unranked at random after them), otherwise at random. Each group plays in a fixed order with a planned referee from the group (group of 4: 1v4 r2, 2v3 r4, 2v4 r1, 1v3 r2, 3v4 r1, 1v2 r3; group of 3: each referees once; group of 2: no referee). Group matches have their own match length (Global Settings → Match length → Groups and cups, default Bo3).
- **The table:** wins, leg difference, legs won, then head-to-head (a mini-table between the players still level, so three-way ties too). Players level after that are marked **level**, and the operator decides with **▲** in Match Controls' group tables; untouched, the group seed decides.
- **Draw the cups:** when the last group match is played, Match Controls shows both seeded fields (group winners, then runners-up; the B cup: thirds, then fourths; ranked across groups per match by win rate, then leg difference, then legs won), a **Play the B cup** switch (on; the B cup needs two players) and **Draw the cups**. Each cup is the existing single elimination with bronze, seeded "All" with the existing `placeSeededPlayers()` (top seed v bottom seed, byes to the best seeds), always drawn with the final in the middle.
- **Placings:** A cup final 1st/2nd, bronze final 3rd/4th; B cup final pair 5th–6th, bronze pair 7th–8th; without a B cup, the A cup's quarterfinal losers 5th–8th by group results (best two 5th–6th). The tournament completes when the last cup final is played.
- **Referees:** planned and filled in when a match starts if the planned player is free; always changeable. Cups, round 1: bye winners first, then the players of the bottom matches, for the top half; the bottom half by the losers of the top half (QF1/QF2 refereed by QF4's players, QF3/QF4 by the losers of QF1/QF2). Later rounds: a loser from the round before; the final: the bronze final's loser. A player can no longer start a match while playing another one (only possible in a group stage).
- **Bracket page:** a **Groups | Cups** switch instead of Finals position. Groups: a card per group with its table (top two marked for the A cup) and its matches with referee and state; a match not yet played opens Match Controls, a played one has **Undo** while it can be undone. Cups: both cups, A above B, with lines, hover, selection, Follow and Undo as in any bracket.
- **Match Controls:** the group stage queue by group (each group's next two matches in their fixed order, with the planned referee chosen), Next up preferring a match whose players and referee are free, and the group tables on the right; then Draw the cups; then the A and B cups side by side, with **Undo the cup draw…**.
- **The three foundations, additions only:** `GROUP_SCHEDULES` beside the DE and SE tables; group matches are in no progression table (they move no one). The cups are `SE_MATCH_PROGRESSION[size]` with renamed IDs (`cupMatchId()`: `FS-2-1` → `A-SF1`; `A-QF1`, `A-B` bronze, `A-F` final, `A-R1-3` in a 16-player cup), joined by `cupsProgressionTable()`; `getProgressionTable()` returns that for this format. A new **`DRAW_CUPS`** transaction. Group matches are locked once the cups are drawn; the cup draw can be undone (`undoCupDraw()`) while no cup match has been started or has a result entered by hand or by the Chalker, which removes the cup matches, their transactions and the draw. Cup matches undo as single elimination does.
- **Stored data, additive:** `tournament.format: 'GROUPS'`, `tournament.groups`, `tournament.cups`, match `side: 'group'|'cup'`, `group`, `cup`, `seId`, `plannedReferee`, `config.legs.groupMatches`. Carried by save, load, import, export and the Analytics preview. `tournament.bracket` holds the group draw, so every "is it drawn?" check works unchanged.
- **Tested in the browser:** complete nights for 6, 7, 9, 10, 11, 13, 15, 16, 17, 18, 23, 25 and 32 players (with and without walkovers in the cups, without a B cup), the cup draw and its undo, undo in the groups and in the cups, a three-way tie and the operator's decision, reload, export and import; double and single elimination regression runs (8–32 players) complete as before.
- Files changed: `js/groups.js` (new), `js/clean-match-progression.js` (`GROUP_SCHEDULES`, `cupMatchId()`, `cupsProgressionTable()`, `getProgressionTable()`, `drawGroups()`, `drawCups()`, `afterDraw()`, completion and rankings for the format, the start checks), `js/bracket-rendering.js` (undo: `isGroupMatchLocked()`, `canUndoCupDraw()`, `undoCupDraw()`; the cup final's gate; Match Controls for the format), `js/bracket-view.js` (`layoutCups()`, the groups page, Groups | Cups), `js/results-config.js` (the format, `groupMatches`), `js/seeding.js` (`forGroups()`), `js/tournament-management.js`, `js/main.js`, `js/newton-history.js` (format labels), `js/dynamic-help-system.js`, `js/types.js`, `tournament.html`, `css/bracket-view.css`, `css/match-controls.css`, `Docs/GROUPS-AND-CUPS.md` (new).

### Undo: undone matches leave the Analytics register again

- **Since v5.1.6, every undo stopped at its last step** with an error (`rolledBackMatchIds.map is not a function`): the list of undone matches is a `Set`, which has no `map()`. The tournament itself was already saved by then, so undo looked fine, but the undone matches were never deleted from the match register, so Analytics could keep counting results that had been undone. Found while testing Groups and Cups; separate commit (`567c35f`) so it can go to `main` on its own.
- Files changed: `js/bracket-rendering.js` (`undoManualTransaction()`).

### Single elimination: finals in the middle

- **The Finals Right | Middle switch now works for single elimination.** In the middle, the bracket is drawn as two halves facing each other: the top half of round 1 from the left, the bottom half from the right, each down to its semifinal, and the final in the middle, level with the semifinals, with the bronze final under it (joined by a dashed line). It is the same single elimination with the same rules; only the drawing differs, so history, undo and everything else are untouched. One setting for both formats (Global Settings → Bracket, or the bracket header). This is the first step towards the season final's A and B cups.
- **The switch shows the right button pressed** for single elimination: it read the layout's name instead of the setting.
- **New pictures for Finals position** in Global Settings → Bracket: format-neutral, showing only where the final goes. **Right:** rounds narrow left to right into the final at the right edge. **Middle:** both sides narrow into the final in the centre. The final is drawn in the dark button colour; the backside shading, which only fits double elimination, is gone.
- Files changed: `js/bracket-view.js` (`layoutSEMiddle()`, `finalsVariant()`, labels and the dashed line, the header toggle), `tournament.html`, `js/dynamic-help-system.js`, `Docs/BRACKET-REDESIGN.md`.

### Release

- Version 5.4.0, "Just Around the Corner": the first format that isn't a knockout, Round Robin (groups and cups, or one group), built on the groundwork of the last releases (one layout per format, the formats registry, seeding, single elimination with the final in the middle). The Chalker is unchanged; its version and caches are bumped with the app's (`CHALKER_VERSION`, `chalker.js?v=26`, service worker cache `chalker-v123`).
- Files changed: `js/main.js` (`APP_VERSION` → `5.4.0`), `chalker/js/chalker.js` (`CHALKER_VERSION` → `5.4.0`), `chalker/index.html`, `chalker/sw.js`, `llms.txt` (Round Robin, single elimination finals in the middle, the new defaults; Round Robin off the "do not invent" list, Swiss, doubles, Cup and Plate and qualifiers on it), `sitemap.xml`, `releases/index.html`, `releases/v5.4.0.html`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.4.0.md`, `js/dynamic-help-system.js` (help review: Top half named as the default, the format called Round Robin in the header help).

## **v5.3.2** — Don't You (Forget About Me) (2026-10-07)

### Match Controls: before the draw

- **Shuffle & Draw is above Seeding** in Match Controls before the draw: the draw buttons come first, the seeding choices under them. Seeding still applies when you draw.
- **Unpaid players can be removed from Match Controls**, with a **×** on their chip, as on Player Registration (the same `removePlayer()`). Paid chips have no ×: mark the player unpaid first, so one stray click can't remove someone who has paid.
- **The draw card no longer promises what it won't do.** It said "Only paid players go into the bracket", then refused the draw with an alert when someone was unpaid. A draw with an unpaid player in the list must never happen: a real player who wasn't marked paid would be left out of the bracket. So while anyone is unpaid, the draw buttons are disabled and say so ("2 players unpaid"), and the note says what to do (mark paid, or remove a player who isn't playing). The refusal in `generateCleanBracket()` stays as the safety net.
- **"Draw an 8-player bracket"**, not "a 8-player", on the draw buttons and in the Seeding panel.
- Files changed: `js/bracket-rendering.js`, `js/seeding.js`, `css/match-controls.css`, `js/dynamic-help-system.js`.

### Setup: the New tournament form stays open when it's the thing to do

- **Completed, or nothing loaded:** the New tournament form is always open, under the heading **Start a new tournament**, with no +/− toggle. Folding it away there removed the one thing the page points at and left no dark button, for a saving of one row. In New and Active it is still the quiet **+ New tournament** bar that opens on request. This also removes the "folded while framed" case (and its CSS rule).
- Files changed: `tournament.html`, `css/setup-page.css`, `js/tournament-management.js` (`syncSetupNew()`), `js/dynamic-help-system.js`, `Docs/PARKING-LOT.md` (item done).

### Setup: one thing to look at

- **Setup has a focal point.** The current tournament is the first thing on the page, and its next step holds the one dark button. **New tournament** is a quiet line under it (**+ New tournament**, with **import a tournament file** beside it) that opens the form when you need it. Once the tournament is **Completed**, the next job is a new one: the form moves to the top and **Create tournament** becomes the dark button, and the recap card below it has plain buttons (Open in Analytics, or Add to Analytics), so there is still one dark button. With nothing loaded, which only happens before a first tournament is created, it looks the same. **One rule marks the focus: the panel to act on is framed** (a mid-grey ring around its border, so it never changes size): the current tournament while it is New or Active, the New tournament form when it is Completed or nothing is loaded. The dark button inside it is the action. The other panel stays plain. Setup only. The form only opens or closes when the state changes, so a redraw never closes it while you type.
- **A flatter current tournament card.** The tinted status band across the top is gone from Setup: the status and its fact (the bracket isn't drawn yet, 12 of 30 matches completed, won by Anna) are on the first line, beside a coloured pill. The four bordered figures are one line of facts (6 players, all paid · 16-player bracket · 2 live now), with a slim progress bar while the tournament is active. The next step sits open on the card under a line, instead of in a box of its own. The status band stays on Player Registration.
- **No more jumping to Player Registration.** Creating a tournament, loading one from the list and importing one used to switch to Player Registration by themselves (an import after 1.5 seconds, which also hid its own success message). Setup stays open instead, and the current tournament card shows the next step, with **Register players →** one click away. An import's message now stays on screen. Setup's Match history used to catch up when you came back to the page, so it now refreshes with the current tournament (`updateTournamentStatus()` redraws it).
- Files changed: `tournament.html`, `css/setup-page.css`, `js/tournament-management.js` (the three `showPage('registration')` calls removed; `currentTournamentStatusFact()`, `syncSetupNew()`, `setSetupNewOpen()`, `toggleSetupNew()`), `js/dynamic-help-system.js`, `Docs/IMPORT_EXPORT.md`.

### One background behind the pages

- **The faint grey frame around the redesigned pages is gone.** The window's background (`#f1f5f5`, a cool grey) differed slightly from the redesigned pages' own (`--nt-bg`, `#f4f3f1`, a warm grey), so a thin strip of the cooler grey showed around Setup, Registration, Analytics and Global Settings. The window now uses the same token; the bracket page, Match Controls and the dialogs look as before. Pages that load `styles.css` without the design tokens (older release pages) keep the old grey through the variable's fallback.
- Files changed: `css/styles.css`.

### Release

- Version 5.3.2, "Don't You (Forget About Me)". The Chalker is unchanged; its version and caches are bumped with the app's (`CHALKER_VERSION`, `chalker.js?v=25`, service worker cache `chalker-v122`).
- Files changed: `js/main.js` (`APP_VERSION` → `5.3.2`), `chalker/js/chalker.js` (`CHALKER_VERSION` → `5.3.2`), `chalker/index.html`, `chalker/sw.js`, `llms.txt` (Setup by state, never a draw with an unpaid player), `sitemap.xml`, `releases/index.html`, `releases/v5.3.2.html`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.3.2.md`, `Docs/PARKING-LOT.md` (the season final's decisions; the SE middle layout as the first formats step).

## **v5.3.1** — Sowing the Seeds of Love (2026-10-06)

### No clock on an analytics-only instance

- **The clock is hidden on an analytics-only instance**, in the app header and in the bracket header: nobody runs a tournament there, so there is nothing to time. Full mode is unchanged.
- Files changed: `css/styles.css`.

### Seeding the draw

- **Shuffle & Draw can keep the best players apart.** A new **Seeding** panel in Match Controls, shown before the draw, ranks the players on earlier tournaments with the same name and seeds the best of them; everyone else is still drawn at random. Seed 1 and 2 go at the top and bottom of the bracket, seeds 3-4 in the two middle quarters, seeds 5-8 in the remaining eighths, and so on, so seeds never meet in round 1.
- **How much is seeded follows the bracket size:** **1/8** puts a seed in a quarter of the round 1 matches, **1/4** in half of them, **1/2** in every one (the rest drawn at random), and **All** seeds every ranked player, with the top seed meeting the bottom seed, the second seed the second-last and so on. In an 8, 16 or 32 player bracket that is 2 / 4 / 8 seeds for 1/4, and 4 / 8 / 16 for 1/2. Fewer than two seeds means no seeding.
- **Byes go to the best seeds,** in rank order (no setting). In All that follows from the mirror draw: the missing lowest seeds are the byes, so they fall to the top seeds. Players with no ranking are never seeded (in All they take the places left over, at random) and only get a bye if there are more byes than seeds.
- **The panel says who is seeded:** how many of the players, which fraction of the bracket, the seeds with their points, who has no ranking, and how many byes the best seeds get.
- **The ranking is a mini-lens.** It is the Leaderboard's points (its point mode and layers) over the tournaments in this browser. The name word is taken from the active tournament's name: numbers, dates and words like "week", "uke" and "Final" are dropped, and the longest word left is used, so "Måndagscup", "Måndagscup week 43", "NewTon Måndagscup" and "Måndagscup Final" are the same cup. Tournaments with "Final" in the name never count, so a Final is ranked on the season's cups. The period is the current half-year, or the previous one while the current has no tournament of the cup (the first match of a season). The word, the period and the amount seeded can be changed per draw, and tournaments can be ticked by hand, so a name that matches nothing can still be seeded by choosing the tournaments. No match or no history gives a random draw, and the panel says so.
- **Settings:** Global Settings → Tournaments → **Seeding**: *Use seeding* Off / Available / On (default Off: nothing changes for a club that doesn't want it), and *Seeded players* 1/8, 1/4, 1/2 or All (default 1/4). Both are new optional settings (absent means off), so older settings read as before.
- **The draw keeps a record:** `tournament.seeding` (the amount seeded, who was seeded with rank and points, the name word, the period and the tournaments used), absent for a random draw. It is kept in saved tournaments and in exports, and read back by every way of loading one.
- **Built to be reused:** `js/seeding.js` decides who the seeds are and knows nothing about brackets; where they go is one function beside the shuffle (`placeSeededPlayers()`), used only when there are seeds. Without seeds the draw is exactly the code it was. A group draw can take the same list later.
- Files changed: new `js/seeding.js`; `js/clean-match-progression.js` (`placeSeededPlayers()`, an optional `seeding` for `createOptimizedBracketV2()`, the record kept at the draw), `js/newton-history.js` (`seedingTournaments()`, `seedingRanking()`, `seedingPlayerKey()`, `halfYear()` made public), `js/bracket-rendering.js`, `js/results-config.js`, `js/main.js`, `js/tournament-management.js`, `js/types.js`, `js/dynamic-help-system.js`, `tournament.html`, `css/match-controls.css`.

### Remote backup: a verified key is locked

- **Once Test connection finds the API key accepted, the server address and the key are locked** in Global Settings → Remote backup, with a note "Verified. The address and key are locked, so they can't be changed by accident." **Change…** asks first, then clears the key and unlocks both; Test connection locks them again. Until a key has been verified the fields stay editable, and a failed test never locks or unlocks anything. The lock is part of the form like any other change: it takes effect with **Save changes**, and **Discard** puts it back.
- **Stored as `server.remoteVerified`**, a new optional flag (absent means not verified, so older settings read as before). It is only kept while an address and key are present, and is left out of exported tournament files with the key.
- Files changed: `tournament.html`, `css/config-page.css`, `js/config-page.js`, `js/results-config.js`, `js/tournament-management.js`, `js/types.js`, `js/dynamic-help-system.js`.

### Help buttons work on an analytics-only instance

- **The Help buttons in Analytics and Custom Settings now open the help.** The help system had been switched off in analytics mode back when Analytics had no help topics of its own; the topics came later, but the switch stayed. F1, Esc and Ctrl+H work there too. The first-run welcome guide is for the Tournament Manager, so it is still not shown on an analytics-only instance.
- Files changed: `tournament.html`, `js/dynamic-help-system.js`.

### Bracket header on an analytics-only instance

- **The status line no longer runs into the Finals buttons** when a bracket is viewed on an analytics-only instance. The header's top row takes its height from the centre group (the tabs and Leaderboard), as the left and right groups span both rows; with those buttons hidden in analytics mode the row collapsed, and the status row rose into the tools. The centre group now keeps the tabs' height (38px) when empty. Full mode is unchanged.
- Files changed: `css/bracket-view.css`.

### Analytics: Finishes chart fits its frame

- **The Finishes chart no longer spills out of its frame**, which showed full screen: the Winner band climbed over the player chips and the 17th+ band ran over the month labels. The bands were spaced as five gaps with each band a gap tall, so the outer two stuck out by half a gap, a tenth of the chart's height. The six bands now share the plot equally, each tier's mark in the middle of its band.
- Files changed: `js/newton-charts.js`.

### Release

- Version 5.3.1, "Sowing the Seeds of Love". The Chalker is unchanged, but its version and caches are bumped with the app's (`CHALKER_VERSION`, `chalker.js?v=24`, service worker cache `chalker-v121`).
- Files changed: `js/main.js` (`APP_VERSION` → `5.3.1`), `chalker/js/chalker.js` (`CHALKER_VERSION` → `5.3.1`), `chalker/index.html`, `chalker/sw.js`, `llms.txt` (seeding, the locked backup key), `sitemap.xml`, `releases/index.html`, `releases/v5.3.1.html`, `Docs/ReleaseNotes/RELEASE-NOTES-v5.3.1.md`.

## **v5.3.0** — A View to a Kill (2026-10-04)

### Custom Settings on an analytics-only instance

- **On an analytics-only instance, Global Settings is called Custom Settings and shows only the Points.** Members use it to try other point values and see them in Analytics with **Custom**; the description says so, and that the values are kept in their browser only and the club's results don't change. The Club panel, the section list and the section heading are hidden there; the full app is unchanged. The names switch with two small classes in `css/components.css` (`.nt-in-full`, `.nt-in-analytics`), so both versions sit side by side in the markup.
- **"Today's values" is gone from the Custom points**, as it was unclear where they came from: the panel's choice reads **From Global Settings** (**From Custom Settings** on an analytics-only instance), and the button **Custom: Global Settings values** (**Custom: custom values**).
- Files changed: `tournament.html`, `css/components.css`, `css/config-page.css`, `js/newton-history.js`, `js/dynamic-help-system.js`, `llms.txt`.

### Analytics → Players: at most six ticked

- **The Players list opens on the best player in the Lens** (or everyone sharing the top points, at most six), instead of ticking everyone.
- **At most six players can be ticked,** the most the charts compare. With six ticked, the other boxes are greyed out ("Compare up to six players: untick one to swap"). The note "N players are ticked; the charts show the six highest ranked" is gone, as it can't happen any more.
- **The box at the top ticks the six best players** in the Lens (it shows ticked when exactly they are), or unticks everyone.
- **The selection lasts for the visit,** like the Lens: it is no longer saved in the browser (the value older versions saved, `newton_analytics_playerSelection`, is cleared with the old Lens values). Players who drop out when the Lens changes are unticked; with nobody left, the best player is ticked again.
- Files changed: `js/newton-history.js` (`_bestPlayers()`; the save and restore of the selection removed), `tournament.html` (the list's hint), `js/dynamic-help-system.js`.

### Analytics: Finishes chart draws lines

- **The Finishes chart (Analytics → Players) draws a line through each player's finishes**, with the marks on top, like the Average chart. It showed marks only, which made a player's run hard to follow, especially when comparing. A tournament a player missed is bridged, as on the other charts; the hollow marks on the axis still show which. The card's sparkline was already a line.
- Files changed: `js/newton-charts.js`.

### Analytics instance: no Match Controls from a selected match

- **On an analytics-only instance, the bracket's selection bar no longer offers Match Controls.** Clicking a match in a tournament opened from Analytics showed the bar with a Match Controls button, which opened Match Controls for the tournament. The header's tabs were already hidden there; the bar's button now is too (`.mode-analytics`, as for the header). Follow and the rest of the bar are unchanged.
- Files changed: `css/bracket-view.css`.

### No stray focus ring

- **A clicked button no longer turns up with a thin blue border.** A clicked button keeps the focus, and the browser draws its focus ring as soon as a key is pressed: an arrow to switch tabs, Esc to close a dialog opened from it, + / −. A key now releases a button's focus first, on every page. Tab, Enter, Space and the modifier keys are left alone, so keyboard use is unchanged.
- Files changed: `js/main.js`.

### Tournament status at a glance

- **Setup's current tournament and Registration's next step have a status band across the top**, tinted by status: grey **New** · the bracket isn't drawn yet; orange **Active** · 12 of 30 matches completed; green **Completed** · won by (the winner). One helper, `currentTournamentStatusBand()`, draws it for both. The small status pill on Setup is gone. (A coloured heading with a 4px top edge was tried first; the maintainer preferred the band.)
- **"Setup" is now "New" wherever the status is shown** (the tournament list, the Load, Delete, Import, Export, Reset and Analytics import dialogs): `tournamentStatusLabel()` maps the stored `setup` to New. The stored status is unchanged.
- Files changed: `js/tournament-management.js` (`currentTournamentStatusBand()`), `js/player-management.js` (`renderRegistrationNext()`), `tournament.html`, `css/setup-page.css` (`.st-band`), `css/registration-page.css`, `js/dynamic-help-system.js`.

### Developer Console: a tab on the bracket page

- **The Developer Console is the third tab, Console**, next to Bracket | Match Controls, shown when the Developer Console is enabled in Global Settings. It replaces the Console link and the dialog. Like Match Controls it is a layer over the bracket; the bracket's tools and legend hide while it shows. The page never starts on it, and turning the console off in Global Settings hides the tab (and leaves it if it was showing).
- **Redesigned in the new design language** (`css/dev-console.css`), decided without a mockup while the maintainer was away:
  - **Left:** a panel with the status figures as items, each with its live value and a coloured dot for health (instead of ✅ ⚠️ 🔴): Overview, Transactions, Matches, Players, Lanes, Storage. Then the commands, grouped **Inspect** (Transaction history, Transaction log, Validate everything, QR payload inspector), **Repair** (Re-render bracket, Recalculate rankings, Refresh all dropdowns) and **Change** (Toggle read-only, Late registration in amber, Reset all config in red). The item showing is highlighted; a sub-view (the pruning preview) counts as its parent. "Updated hh:mm:ss · every 2 seconds" at the bottom.
  - **Right:** the current view in a white panel, over **Console output** (collapsed; opens to up to 40% of the height, with the line count, Copy and Clear).
  - The views' repeated inline styles became classes (`dc-title`, `dc-box`, `dc-boxhead`, `dc-lines`, `dc-link`, `dc-btn`, `dc-input`); rounded boxes, the app's fonts and buttons. The eleven "← Back to Overview" links are gone, as the left panel is always there. What each view shows, and every command, is unchanged.
- **It runs only while its tab shows:** `startDeveloperConsole()` (capture `console.log`, draw, refresh every 2 seconds) and `stopDeveloperConsole()` replace `openAnalyticsModal()` / `closeAnalyticsModal()`; `showBracketView()` calls them, and so does leaving the page. The scroll listeners are added once.
- The late registration hints (Player Registration) and the help point to the Console tab.
- **Match progression is removed** (the view, its filters and Progression Code): the bracket shows progression far better. About 430 lines. The Progression Integrity check in Validate everything stays. `getMatchProgressionText()` in `bracket-rendering.js`, left over from the old bracket cards with no callers, went with it.
- Files changed: `css/dev-console.css` (new), `tournament.html` (the view replaces `#analyticsModal`; tab; stylesheet link), `js/analytics.js`, `js/bracket-rendering.js` (`showBracketView()` knows three views), `js/bracket-view.js` (the tab follows the setting), `js/main.js`, `js/player-management.js`, `js/results-config.js`, `js/dynamic-help-system.js`, `css/bracket-view.css`, `css/styles.css` (old console rules removed), `Docs/BRACKET-REDESIGN.md`, `Docs/DIALOGS.md`.

### Match Controls in the bracket's frame

- **Match Controls is no longer a dialog**: it shares the Tournament Bracket page's frame, switched with the tabs **Bracket | Match Controls** in the header (mockup: frame-views.html, approved 2026-10-04). It is a layer over the bracket, so the bracket keeps its camera underneath. The results-waiting badge (network handover) moved onto the Match Controls tab.
- On Match Controls, the bracket's tools (Finals, Fit all, zoom) and the colour legend are hidden; the clock and the status line stay.
- **Leaderboard** beside the tabs has the tabs' height and text size, so it no longer looks like a stray, smaller tab.
- Match Controls' own header and footer are gone. **Scan QR results** is in the Lanes heading (QR handover, while matches are live); Leaderboard is the header's button; Close is the Bracket tab.
- **"Open Match Controls with the bracket" is now "Start on Match Controls"** (Global Settings, same stored setting `config.ui.autoOpenMatchControls`, on by default): the page opens on Match Controls, or on the bracket when off. A tournament opened from Analytics for viewing still opens on the bracket.
- **The left and right arrow keys switch between Bracket and Match Controls** (not the Console, and nothing while it shows). Not while typing or choosing in a list, with a dialog open, or in analytics-only mode.
- The bracket's selection bar **Match Controls** switches to Match Controls and flashes that match's tile or queue row (`showBracketView('controls', id)`).
- The layer keeps the id `matchCommandCenterModal` and `style.display = 'block'`, so every existing "redraw Match Controls if open" check works unchanged, including those in `clean-match-progression.js` (not touched). Two checks that didn't fit are fixed: `qr-bridge.js` looked for Match Controls on the dialog stack, and the tournament reset in `tournament-management.js` only accepted `display: flex` (so it never refreshed).
- When the last dialog closes, `popDialog()` redraws Match Controls if it is showing. As a dialog, Match Controls was restored by the stack after the winner dialog, a QR result and so on, and that restore was its redraw; this keeps it (e.g. a finished match leaves the lanes board straight away).
- **Fix:** after a reload on the bracket page, Start on Match Controls was ignored. The page is restored before the tournament loads, so the start view saw no tournament and chose the bracket (the old auto-open had a 100 ms delay that hid this). `showBracketStartView()` in `main.js` makes the choice, from `showPage()` and once more at startup after the tournament has loaded.
- Decided: the start view applies every time the page is opened, as the auto-open did. Match Controls' links (Player Registration, Global Settings, Tournament Analytics) no longer pop a dialog first.
- Files changed: `tournament.html`, `css/bracket-view.css`, `css/match-controls.css`, `js/bracket-rendering.js` (`showBracketView()`, `_bvSetView()`), `js/bracket-view.js`, `js/main.js`, `js/qr-bridge.js`, `js/tournament-management.js`, `js/results-config.js`, `js/dynamic-help-system.js`, `js/types.js`, `Docs/BRACKET-REDESIGN.md`.

### Global Settings: Test connection

- **Remote backup has a Test connection button**, next to the API key. It tests the address and key as they are in the form (saved or not), through this computer's relay, the way a backup travels, and says what it found: connected and the key accepted; the key wrong or missing; the server doesn't ask for a key (with a nudge to set one); it can't be reached (with the reason); it asks for a web-server login; or it is an older version that can't be checked.
- **New `api/key-check.php`:** says whether the instance asks for an API key and whether the `X-API-Key` header matches. Writes nothing.
- Checked against real containers: right key, wrong key, no key, a server without a key, an unknown host, an older server, no address.
- Files changed: `api/key-check.php` (new), `js/tournament-management.js` (`testRemoteConnection()`), `tournament.html`, `css/config-page.css`, `js/dynamic-help-system.js`, `Docs/REST_API.md`.

### Match Controls redesign

- **Match Controls is rebuilt around the dartboards** (mockup: https://claude.ai/artifact/KKXAavLSXz6QDqm7xEbHoZ), in the new design language (`css/match-controls.css`). Same functions and rules as before: lanes, referees and their conflicts, Start/Stop, the winner dialog, QR and network handover, undo.
  - **Header:** the tournament, Live / Ready / Played / Free lanes (Players / Paid / Unpaid before the draw), the clock and ×.
  - **Lanes:** one tile per live match, in lane order: the players as big **Wins** buttons, the match number, best of, **time on the board** (from the match's Start in the history; updates with the clock), the lane and referee (both still changeable while live), the round, the handover (QR, Transfer, or the pulsing **Result ✓**) and Stop. A live match without a lane gets a **No lane** tile (Transfer needs a lane).
  - **Free lanes collapse into one line**, with the next ready match without a referee conflict: click a free lane to put that match on it and start it (`startMatchOnLane()`, using `updateMatchLane` and the usual Start handler). Lanes not in use are listed.
  - **Ready to start:** a compact queue by round, frontside and backside side by side (one column in single elimination), each row with Lane, Referee and Start; a referee conflict is written on the row and blocks Start, as before.
  - **Referees:** live matches without a referee first, then recent losers, recent winners and who refereed recently (`getRefereeSuggestions()`, unchanged). Redrawn after every action.
  - **Before the draw:** the players as chips to mark paid, an Add field (the player database, via `addPlayer()`), a compact summary of the points, match lengths and lanes, and Shuffle &amp; Draw per format.
  - **Finished:** the podium and, always under it, three headline highlights (Most 180s, Shortest leg, Highest out) with everyone who shares the best, or "None tonight", and a slate plaque at the foot with the club name (in Insignia), the tournament and the date (the panel's small name and date are gone; both columns share a height); on the right, first **4th, 5th–6th and 7th–8th** (the places after the podium that still score placement points), then the night's awards, one per row so there is never a gap however many there are: Most points, Most matches won, Backside run, Best average (from the Chalker matches in the register), Most tons, Lollipops and Busiest lane, each left out when there is no data; then **the night in numbers**, always the same six: Bracket (format and size), Total points, Matches played, Short legs, 180s, High outs; Tournament Analytics, Export and Leaderboard centred at the foot. Dropped before release at the maintainer's request: the longest and quickest match, whitewashes, deciders, busiest referee, a Debut award (replaced by the places 4th–8th), and a Season leader award (a half-year includes tournaments outside the club's season; see Docs/PARKING-LOT.md, "A Season concept").
- The lane and referee dropdowns say **No lane** / **No referee** instead of "No" / "None".
- **Fix:** the old finished view's "Most Achievement Points" always said "None": its helper `calculateAchievementPoints(player)` was shadowed by the points function of the same name in `results-config.js`. The new view uses `calculatePlayerPoints()`.
- About 1,100 lines of old Match Controls code (`createMatchCard`, the old `showCommandCenterModal`, `populateRefereeSuggestions`, the celebration functions, `updateTournamentConfigDisplay`) and about 620 lines of its CSS are gone.
- Files changed: `css/match-controls.css` (new), `js/bracket-rendering.js`, `js/lane-management.js` (No lane), `tournament.html` (Match Controls markup, stylesheet link), `css/styles.css`, `js/dynamic-help-system.js`, `js/types.js`.

### Analytics: As played | Custom

- **The points controls are two buttons: As played | Custom.** As played (the default) scores each tournament as it was played: its own point values, placement and attendance included. Custom opens a small panel: point values (today's from Global Settings, or as played) and whether placement and attendance points count. The button then says what is custom ("Custom: today's values, no attendance"); the choice is kept for the visit. It replaces Original | Current and the Ranking and Attendance switches ("Ranking" is now "Placement points").
- The focus ring on the strip's button groups is drawn inside the button, so the group's edge no longer clips it to an orange sliver.
- Files changed: `tournament.html`, `js/newton-history.js` (`_setPoints()` and the panel replace `switchPointMode()` and `toggleLayer()`), `css/analytics-page.css`, `js/dynamic-help-system.js`, `llms.txt`.

### No more pop-up hints

- **The orange pop-up hints are gone**: "Need more paid players. Press F1…", "Tournament created!", "Bracket generated!", "N matches ready to start", "Many matches are live…", "Tournament completed!" and the rest. The pages show the same things (Registration's next step, the bracket, Match Controls), and anything that blocks an action still says so in its own dialog. Help is still on F1 and the Help buttons.
- `showHelpHint()`, `suggestHelp()`, `triggerContextualHelp()` and the `onTournamentCreated` / `onBracketGenerated` / `onFirstMatchCompleted` hooks are removed from `dynamic-help-system.js`. Their callers elsewhere (including `clean-match-progression.js`, unchanged) check that they exist first, so they now do nothing.
- Files changed: `js/dynamic-help-system.js`.

### Player database

- **A global player database in Player Registration** (mockup: https://claude.ai/artifact/XpVNb6RwbahYizDRmQow1m). Every player has a permanent ID, a **short name** (what the bracket, Match Controls, the Chalker and the Leaderboard show; unique), and a first and last name. It replaces the saved-players list.
  - **Tonight** tab: tonight's players (short name, full name under it), and **Add from the database** with a search over short, first, last and previous names (Enter adds the first match, or offers to create the player), the most active players first. **+ New player** suggests the short name from the first name, with the last name's initial when it is taken.
  - **Player database** tab: every player with their names, previous names, finished tournaments played and last played. **Edit** renames (the old short name is kept as a previous name, so older results still find the player); **Merge…** joins duplicates (one keeps its details; the other's ID and names stay with it); **Archive** hides a player from the pick list. **Players who have been in Analytics can't be deleted**, only archived; players who never played a match can.
  - **First run:** the saved players, and the names in saved tournaments and the Analytics register, become database players (short name = the name; first and last empty to fill in), with a notice saying how many. Not on an analytics-only instance.
- **Tournament players carry the database ID** as `registryId` (additive; their own `id`, used by the bracket, the history and undo, is unchanged). Players in a tournament from before the database are linked by name when it is loaded before the draw.
- **Analytics follows the ID:** each tournament in the register records which database player each tournament player is (`registryIds`, at finalization and import). Analytics groups by it, merged IDs included, and links older tournaments by name (short or previous name). It shows the current short name everywhere (Leaderboard, Players, Dashboard, winners, match lists and match detail) and the full name in the profile.
- **The database travels with tournaments:** exports and uploads carry it (`playerDatabase`, with full names; the `playerList` of short names stays for older versions). Importing a tournament file **adds** its players by ID instead of replacing the saved-players list. The server keeps the newest uploaded copy in `tournaments/registry/player-database.json`, which an analytics-only instance reads (`instanceMode` in the page tells it apart under `?tm`).
- Match Controls' add-player field and late registration (Developer Console) take players from the database too (found by name, or created).
- **Merge guard:** two players who played in the same tournament are two people, so Merge refuses them and names the tournaments (`usage()` now also records which tournaments each player was in).
- Files changed: `js/player-registry.js` (new), `js/player-management.js`, `js/newton-history.js`, `js/newton-db.js`, `js/tournament-management.js`, `js/analytics.js`, `js/main.js`, `js/types.js`, `js/dynamic-help-system.js`, `css/registration-page.css`, `tournament.html`, `api/upload-tournament.php`, `Docs/REST_API.md`, `llms.txt`.

### Player charts

- **A player's progression, as charts** (Analytics → Players; mockup: https://claude.ai/artifact/VqxXWoFhDpuUpMc1hMhZd7). The profile gains their last 10 finishes, and six cards with a sparkline and a one-line verdict ("▲ 2 places in 10", "10.4 last 5, ▼ average 12.8"). A card opens its chart:
  - **Position**: place in the standings after each tournament, by points so far (ranked as the Leaderboard ranks), with the top 16 shaded.
  - **Points**: points each tournament, and the form line (the average of the last 5 played).
  - **Finishes**: each tournament's finish on bands (Winner, Final, Top 4, Top 8, Top 16, 17th+).
  - **Average**: three-dart average each tournament, with the worst-to-best match spread; tournaments without Chalker matches are shaded.
  - **Matches**: won and lost each tournament.
  - **Highlights**: 180s, high outs and short legs on one time axis.
- **Comparing:** "+ Compare with" adds up to five more players, each in its own colour, and "The field" adds the median of everyone who played (Points and Average). With several players, Points shows the form lines and Matches the win rate over each player's last 5 tournaments. **Full screen** shows the same chart, measure and players on the whole screen (Esc closes it).
- **Ticking several players** now compares them on these charts instead of in a table (the six highest ranked when more are ticked).
- Everything follows the Lens and the Points controls. Hover or tap a tournament for its figures. Phones: cards two to a row, charts to the screen's width.
- **Under the hood:** `js/newton-charts.js` (new) draws the charts as plain SVG and never changes data. The figures come from `_computePlayerRows()`, whose per-tournament history now also carries the three-dart average and the worst and best match (checked: the Leaderboard's averages are unchanged), via `_buildChartData()` (tournaments in date order, results per tournament, standings after each, the field).
- Files changed: `js/newton-charts.js` (new), `js/newton-history.js`, `css/analytics-page.css`, `tournament.html` (script), `js/dynamic-help-system.js`, `llms.txt`, `Docs/PARKING-LOT.md`.

### Analytics opens on the current half-year

- **The Lens starts every visit on the current half-year** (H2 2026 = 1 July to 31 December), or on the previous half-year while the current one has no tournaments yet, or on everything if both are empty. Every tournament in it is ticked. What a visitor changes lasts until they leave the page; **nothing about the Lens is remembered between visits any more** (it used to be saved in the browser, so a member could be left looking at an old selection). Show all still shows every tournament.
- Setup's **Open in Analytics** still opens on just that tournament: it now sets the Lens directly for the visit (`scopeTo()`), with no date range, instead of going through the saved selection.
- The Lens values older versions saved (`newton_analytics_scope`, `_textFilter`, `_dateFilter`) are cleared on the first visit.
- Files changed: `js/newton-history.js` (`_initLens()`, `scopeTo()` new; the Lens save/restore functions removed), `js/dynamic-help-system.js`, `llms.txt`.

### Tournament Setup: New tournament on top

- **The New tournament form is now one row at the top of Setup** (title, name, date, Create tournament, with Import tournament under it), and the current tournament is full width under it. Before, the form sat to the right of the current tournament and was easy to miss. Tournaments and Match history are unchanged.
- **Tournaments and Match history now end at the same line.** Both panels take the row's height and each list fills its panel (scrolling past 470px as before). They used to size to their own contents, so their bottoms differed.
- Files changed: `tournament.html` (order of the two panels), `css/setup-page.css`, `js/dynamic-help-system.js` (Setup overview).

## **v5.2.1** — Open Bar, Locked Cellar (2026-10-04)

### API key for writes

- **`NEWTON_API_KEY`** (new, optional): when set, every API request that changes something needs the `X-API-Key` header to match: Upload, Delete, Relay, and saving corrections (`POST api/corrections.php`). Reading never does: the page, List Tournaments, the files in `/tournaments/`, and reading corrections. A missing or wrong key gets 401. Unset means no key, as before. Compared with `hash_equals()` in `require_api_key()` (`api/api-check.php`).
  - Purpose: a public analytics-only instance that members open without a login, which only the club's backups can write to. Until now the whole site had to sit behind a proxy login to keep strangers from uploading or deleting.
- **`?tm` works as before:** with the correct `NEWTON_TM_PASSWORD`, `tournament.html` hands the instance's key to the page (`NEWTON_CONFIG.apiKey`), and the page's own writes (corrections, delete, Backup to server, shared upload) send it via `apiWriteHeaders()`. Only behind a password: with an open `?tm` the key is never handed out.
- **Global Settings → Server & backup → Remote backup:** **API key** replaces Username and Password. The relay sends it to the remote server as `X-API-Key` instead of basic auth. The remote username and password are removed from the saved settings the next time they're saved; nothing needs migrating.
- **Server credentials stay out of tournament files.** Tournament exports and uploads carried a full copy of the settings, including the remote username and password; on a public instance those files are readable by anyone. The copy now leaves out the API key and the old username and password (`configForExport()` / `withoutServerCredentials()` in `results-config.js`), as does the config snapshot in the Analytics register (`newton-db.js`). **Tournament files uploaded before v5.2.1 may still contain the old remote username and password**: retire those credentials when switching to the API key.
- **Network handover is off on analytics-only instances:** with `NEWTON_MODE=analytics`, the handover endpoints answer 403. They are for the club's own network.
- CORS allows the `X-API-Key` header on the write endpoints.
- Docs: DOCKER-QUICKSTART.md (analytics-only section, environment table, Security), `Docs/REST_API.md` (new Authentication section, Relay, settings), `api/README.md`, `Docs/NETWORK-LAYER.md`, the compose files, `automatic-backup.html`, `docker-quickstart.html`, `rest-api.html`, `llms.txt`, and the in-app help.
- Files changed: `api/api-check.php`, `api/upload-tournament.php`, `api/delete-tournament.php`, `api/relay.php`, `api/corrections.php`, `licensed/api/v1/_common.php`, `tournament.html` (`?tm` key hand-over, Remote backup API key field), `js/tournament-management.js` (`apiWriteHeaders()` new), `js/newton-history.js`, `js/results-config.js`, `js/newton-db.js`, `js/types.js`, `js/dynamic-help-system.js`, `js/main.js` and `chalker/js/chalker.js` (version), `chalker/index.html` and `chalker/sw.js` (cache).

## **v5.2.0** — New Flights, Same Arrows (2026-10-04)

### App header and footer

- **The app header and footer use the new design language**, as agreed in v5.1.10 for the redesign of the remaining pages (Docs/BRACKET-REDESIGN.md, "App header"). The header runs the full width: white, with one hairline underneath. It has a 40px round logo, the club name in Inter, and the tournament name and date. The clock is the same as the bracket header's. The name, date and clock are centred vertically in the whole header, as on the bracket page. The page links are plain text, with the current page bold and underlined in ink. The footer is full width with a hairline above, and on short pages it sits at the bottom of the window. This is done as one stylesheet over the existing markup, with no markup or script changes, so it can be removed in one line.
- **The header title is set in Insignia Regular** (20px), the club's display face, instead of Inter bold. It was tried and kept; Insignia has only a regular weight, so the title is not bolded.
- **Two shared page widths** in `css/design-tokens.css`: `--nt-page-max` (1520px, forms and short lists) and `--nt-page-wide` (1840px, table-heavy pages), for a full-screen browser of at least 1920px. Setup, Registration and Global Settings use the first.
- Files changed: `css/app-header.css` (new), `tournament.html` (stylesheet link), `css/design-tokens.css`, `css/setup-page.css`, `css/config-page.css`, `Docs/BRACKET-REDESIGN.md`.

### Tournament Setup redesign

- **The Setup page is rebuilt in the new design language** (mockup: https://claude.ai/artifact/Urawx3Mei3FK7SisBE22gd).
  - **Current tournament** comes first: name, date, format, a status pill (Setup / Active / Completed) and four figures. The figures are players (and how many have paid), bracket size, matches completed (with a progress bar), and the matches being played now with their lanes (the winner, once completed).
  - **Next step** changes with the status: Register players / Open bracket / Open in Analytics. The last becomes "Add to Analytics" when a completed tournament isn't in the Analytics registry.
  - Export, Backup to server and Reset tournament moved into this panel as quiet links, since they act on the loaded tournament.
  - **New tournament** is its own form, with Import tournament under it.
- **Tournaments is one scrolling table**, newest first. "Show All / Show Less" and the separate Shared / My Tournaments lists are gone. When a server answers, "This computer" and "Server" are two tabs; the Server tab lists the shared tournaments with Import / Re-import and ×. The loaded tournament is marked "Loaded" and has no Load or × button, so the "can't delete the active tournament" alert can no longer be reached. Storage is a small meter in the table header, still opening the storage dialog.
- **Match history** is one entry per match: the winner in green, "beat", the loser and the score (winner's legs first), then lane, referee and where both players went ("Ida to FS-2-2, Siri to BS-1-2", "out (7th-8th)", "wins the tournament"). Walkovers between two empty slots (no one moves on) are left out; the bracket still shows them. The match number is a tag showing the side: frontside outlined, backside grey, grand final dark. It follows the tournament's format: single elimination previously showed no progression, and its bronze final reads "takes 3rd place" / "takes 4th place".
- **Fix: the tournament list could show stale data.** `loadRecentTournaments()` overlaps itself, because each call waits for the server check. An earlier call that finished late overwrote the list with what it had read before a save. Only the latest call renders now. The table no longer waits for the Analytics registry (IndexedDB) either: it is drawn first, and the Analytics column is filled in when the registry answers.
- Reset tournament now refreshes the Setup page (status, match history, tournament list).
- In-app help for Setup rewritten for the new page.
- About 440 lines of old Setup CSS removed from `styles.css` (Setup layout, Recent Tournaments, match history cards, storage link, Analytics labels, plus unused `.tournament-item` / `.match-result-item` rules).
- Files changed: `css/setup-page.css` (new), `tournament.html` (Setup markup, stylesheet link), `js/tournament-management.js` (`renderSetupCurrent()` new; `loadRecentTournaments()`, `updateStorageIndicator()`, `updateTournamentStatus()`, `showImportStatus()`, `confirmReset()`, `loadSharedTournaments()`; `toggleSharedTournamentView()` / `toggleLocalTournamentView()` removed), `js/main.js` (`updateMatchHistory()`, `getPlayerProgressionForDisplay()`, `showPage()`), `js/dynamic-help-system.js`, `css/styles.css`.

### One points formula

- **Points are calculated in one place.** Participation + placement + achievements was written out separately in `calculatePlayerPoints()` (Registration, the Leaderboard dialog, exports) and three times in Analytics (the Leaderboard, the Dashboard's Points total, and per-match achievement points in the Register). Now `calculatePoints(stats, placement, pointValues, include)` and `calculateAchievementPoints(stats, pointValues)` in `results-config.js` do it, with `PLACEMENT_POINT_KEYS` for placement → point setting. Analytics passes its Ranking / Attendance layers as `include`, and its point mode (Original / Current) as `pointValues`; `calculatePlayerPoints()` stays as the Global Settings wrapper.
- No visible change: checked against the old code on 20,000 random players and tournaments, with identical results. Two edge cases now behave consistently: a missing point value counts as 0 instead of making the total `NaN`, and the Dashboard's Points total is the sum of the players' points (it previously also counted any placement entry without a matching player).
- Files changed: `js/results-config.js`, `js/newton-history.js` (`_achPoints()` and `_placementKeys` removed).

### One Leaderboard look

- **The Leaderboard dialog** (Leaderboard in the bracket header and Match Controls) **now looks the same as the Registration Leaderboard.** Both were already filled by `updateResultsTable()` with the same rows; only the styling differed.
  - The table style moved from `css/registration-page.css` (`.rg-lb`) to a new `css/components.css` as `.nt-leaderboard`, used by both.
  - The dialog is rebuilt on the `.dlg` pattern ("Leaderboard", the table edge to edge, Close), with no exports. Clicking a player still opens the statistics editor.
  - The dialog stack no longer shows `statisticsModal` as `flex` (a leftover from the old box's centring, which stretched the new dialog to the window height); it is `block` like every other dialog.
- The old `.results-table` CSS (dark header, stripes) and the dialog's old box rule are removed from `styles.css`; nothing else used them.
- Files changed: `css/components.css` (new), `css/registration-page.css`, `css/styles.css`, `tournament.html`, `js/main.js` (`pushDialog`), `js/results-config.js` (empty row).

### Player Registration redesign

- **The Registration page is rebuilt in the new design language** (mockup: https://claude.ai/artifact/TnxY3JWiqVPWJ7CpoPUXAU). It behaves the same way; the registration process itself will be reworked later.
- **Next step strip:** Players / Paid / Unpaid counts, then what to do now.
  - Before the draw: "Draw the bracket" with Open bracket. The button is disabled below the formats' minimum of paid players or above their maximum; the limits are read from `TOURNAMENT_FORMATS` via `getVisibleFormats()`.
  - After the draw: the matches still to play.
  - With no tournament loaded: Go to Setup.
- **The "Need N more paid players" / "You now have enough players" pop-ups are gone** (`showHelpHint` calls in `addPlayer()` and `togglePaid()`); the next step shows the same thing all the time.
- **Players** are rows in as many columns as fit (three at 1920px; more after the draw, when rows show names only), with a Paid / Unpaid pill. Clicking a row toggles paid, and × removes an unpaid player, as before. The name field and Add player sit at the top of the list. After the draw the list is locked: rows have no click or ×, so the "tournament in progress" warning can't be reached from it. "Player arrived late?" sits under it.
- **Saved players** shows only the saved names not in the tournament, as chips: click a name to add the player, × to delete the name from the list. The separate "In Tournament" list is gone (the Players list shows them), and with it `removePlayerFromTournament()`. A paid player is now removed by marking them unpaid first.
- **Payment QR** has its own panel, hidden when `images/payment.png` is missing.
- **After the draw the Leaderboard takes the wide column**, with Export CSV / JSON in its header. Clicking a row still opens the statistics dialog.
- **Fix: reloading the Registration page during a tournament showed the pre-draw layout.** The page was restored before the tournament had loaded; `autoLoadCurrentTournament()` now redoes the layout and the saved players once it has.
- **Fix: "Player arrived late?" told you to click the version number** in the bracket's lower-right corner, which went with the CAD box in v5.1.10. It now points to the Console link in the bracket header.
- In-app help for Registration rewritten. About 430 lines of old Registration CSS removed from `styles.css` (layout, player cards, saved-player items, `.scrollable-column`, `.two-column-container`, `.btn-small`), keeping the `.results-table` styles the statistics dialog uses.
- Files changed: `css/registration-page.css` (new), `tournament.html` (Registration markup, late-registration text, stylesheet link), `js/player-management.js` (`renderRegistrationNext()` new; `updateRegistrationPageLayout()`, `renderPlayerList()`, `updatePlayersDisplay()`, `updatePlayerCount()`, `addPlayer()`, `togglePaid()`, `showLateRegInfoModal()`; `removePlayerFromTournament()` removed), `js/dynamic-help-system.js`, `js/analytics.js` (comment), `css/styles.css`.

### Analytics: new content

- **Dashboard:** under the tiles, the Leaderboard's top 10 (click a player to open their profile) and the latest ten tournaments with their winner (click one to open it in the Register).
- **Player profile:** one ticked player now shows their Leaderboard figures (points, played, matches, legs, average, best out, best leg, 180s, high outs, short legs), their rank in the lens, how often they finished 1st to 7th–8th, and every tournament they played with their placing, 180s, match W–L and points (click one to open it). Comparing several players shows the same figures side by side; click one to see them alone.
- **Winner** column in the Register's tournament list, and Winner in an opened tournament's facts.
- **Leaderboard:** Points and Played come right after the player's name.
- **One set of player figures:** the Leaderboard's per-player calculation is now `_computePlayerRows()`, used by the Leaderboard, the Dashboard's top 10 and the Players list and profile. The Players list had its own count of tournaments and W–L; it now uses the same rows, so all views agree. The rows also carry one entry per tournament (placing, stats, points, W–L), which the profile lists.
- Files changed: `js/newton-history.js` (`_computePlayerRows()`, `_dashboardPanels()`, `_renderProfile()`, `_winnerName()` new), `css/analytics-page.css`, `js/dynamic-help-system.js`.

### Analytics redesign

- **The Analytics page is rebuilt in the new design language**, at the wide page width (mockup: https://claude.ai/artifact/3L3xkvRYMGJLcXyCGCatdZ). It shows the same things; new content (Dashboard top 10 and latest tournaments, a fuller player profile, a Winner column, Points and Played next to the name) follows separately.
- **One strip for the controls:** the view buttons (Dashboard / Leaderboard / Players / Register) and Points (Original / Current, Ranking, Attendance) on the first row; on the second, how many tournaments are counted and why ("23 of 24 tournaments · 1 unticked"), and the Lens. The strip turns orange when not everything is counted. "Choose tournaments" opens the Register's tournament list.
- **The Lens moved out of Register → Tournaments to the strip**, so it is visible and works from every view (name, from / to, the half-year buttons, now labelled "H2 2026", and Show all, formerly Reset). The tick boxes for single tournaments stay in the Register. The ticks are now set up before any view draws (`_ensureChecked()`), so the Lens works before the Register has been opened, and changing it redraws the view on screen (`_refreshActiveView()`).
- **Dashboard:** the seven numbers as tiles, still clickable.
- **Leaderboard:** the Leaderboard look, with group headings (Placements, Achievements, Total, Best, Matches, Legs). A line under the 16th player marks the top 16 while the table is in points order (it used to tint the top 16 rows whatever the order). Export CSV / JSON in the panel header.
- **Players:** the list shows Played and W–L; unticked players are dimmed. One ticked player shows a profile, several a comparison.
- **Register:** Tournaments / Matches tabs and a path back from an opened tournament or match. Tables show the match number as a side tag (as in Match History), the winner in bold, and Chalker / Manual as a pill. An opened tournament shows Format, Players, Matches and Points.
- **Match detail** (Analytics, and the match dialog opened from Match History): the match number, length and how it was scored, then a large score line, the achievements table, and the legs for Chalker matches. The match dialog is rebuilt on the `.dlg` pattern.
- **Phones:** Analytics is the page club members open on their phones (the analytics-only instance), so it has a phone layout below 700px: the controls stack, the tiles go two to a row, columns that don't fit are left out (Format, Matches, Date, Type, the buttons), and the Leaderboard scrolls sideways with rank and name kept in view. The app header and footer run edge to edge with the page links scrolling sideways, and Global Settings (Club and Points) fits a phone as it was.
- **NewtonTable** (only Analytics uses it): optional column groups (`group`), a class for heading and cells (`columnClass`), a scroll box around the table, and the new look in `css/components.css`. Pagination stays, with Rows 10 / 25 / 50 / All as a segmented control. The Players list no longer hides its page controls (with more than 25 players the rest could not be reached).
- **One match-number tag:** `matchIdTag()` in `main.js` makes the side tag for Match History and Analytics; its style moved from `setup-page.css` to `.nt-id` in `components.css`.
- In-app help for Analytics updated (where the Lens is, Show all, the top-16 line, the Players tick boxes).
- About 740 lines of old Analytics CSS removed from `styles.css`, including a narrow-screen block that also changed `.container`, `.page` and `.nav` on every page below 1120px.
- Files changed: `css/analytics-page.css` (new), `css/components.css`, `css/app-header.css`, `css/setup-page.css`, `css/styles.css`, `tournament.html` (Analytics markup, match dialog, stylesheet link), `js/newton-history.js`, `js/newton-table.js`, `js/main.js` (`matchIdTag()`), `js/dynamic-help-system.js`.

## **v5.1.10** — Straight Down the Middle (2026-10-04)

### New bracket view

- **The bracket is drawn by a new bracket view** (double elimination first; single elimination below). The layout is derived from the progression table instead of being written out per bracket size, so 8, 16 and 32 players share one set of rules and one card. Fit all shows the whole bracket on one page and is the furthest you can zoom out; scroll (or pinch) zooms at the cursor and dragging pans, and the bracket can't be dragged off-screen. Hovering a match magnifies it when zoomed out, or shows where its winner and loser go when zoomed in. Clicking a match selects it and highlights its lines, with markers pointing to connected matches that are off-screen; **Follow** traces a player through the bracket. The finals can sit on the right, as before, or in the middle (**Finals: Right | Middle**, remembered in the global config as `ui.bracketFinals`; missing means right). The same setting is in Global Settings → Bracket as **Finals position**; changing it in either place changes it in both.
- **The bracket page has a header instead of floating buttons.** On the left, the tournament name (large) and date with links to Setup, Registration, Config and Analytics (plus Console when the Developer Console is enabled in Config); in the centre, Match Controls and Leaderboard; on the right, the Finals toggle, Fit all and zoom, and a large clock spanning both rows (the bracket runs full screen, so the OS clock isn't visible). Below, centred, a status line (players, matches, played, walkovers, live, ready) and a legend. The CAD info box and the bottom watermark line are gone from the bracket page, as is the hover status text in the bottom centre.
- **Cards have no controls.** Lane, referee, start and winner are handled in Match Controls. **Undo** moved from clicking the winner on the card to an **Undo match** button in the selection bar. It appears only when the match can be undone, next to a **Match Controls** button that is always the rightmost in the bar; the eligibility check and the undo itself are unchanged (`isMatchUndoable()`, `handleSurgicalUndo()`).
- **Club name in the bracket:** the club name from Config is shown large on the side labels' line, top left with the finals on the right, and centred on the edge of the backside's shaded area with them in the middle.
- **Fix: zooming the bracket above 100% made the whole page flicker.** The zoom layer was kept as one GPU layer (`will-change: transform`); at high zoom a large bracket outgrew the graphics memory and Chrome kept redrawing the page. It is now painted normally (only the visible part is drawn, and text stays sharp at every zoom), and the bracket area is paint-contained.
- **Selection bar round names match the bracket's headers** ("Frontside final", "Semifinal", "Bronze final"…), instead of Match Controls' wording (which called the frontside final "Frontside Semifinal").
- **Long tournament names are cut short with …** before the status line; hovering the name shows it in full.
- **Labels:** FRONTSIDE and BACKSIDE are larger and centred over each side's first round, with FINALS just above the grand final in both layouts; round and finals headers are larger and bolder ("3rd place" now sits under the backside final), centred over their column, each the same distance above the topmost match of its round; the signature is centred under round 1 with the finals on the right (left-aligned at the bracket's edge with them in the middle).
- **Round 1 losers:** with the finals on the right, the lines from frontside round 1 to backside round 1 are always drawn, mirroring the frontside forks (the gap between the two halves now equals the gap between rounds). Other loser lines appear when a match is selected.
- **Finals on the right:** the grand final is level with the frontside final (a straight line between them) and the backside final sits directly below it. The frontside final feeds both finals: the winner's line runs straight on to the grand final, the loser's line drops from the bottom of the card into the backside final, always drawn. The line from the last backside match leaves from its left side (the backside runs right to left), goes round under the bracket and up the right side, with a T-junction into the backside final, and continues into the grand final from the right, so the two halves meet there from both sides. The backside's shaded area extends to take in that line where it runs alongside and under the backside.
- **Finals in the middle:** the grand final is now on top and the backside final below, matching the finals on the right. The frontside final is level with the grand final and the backside's last match level with the backside final, so both feed in with straight lines. The frontside final's loser line drops from the bottom of the card into the backside final, always drawn, as with the finals on the right.
- **Line colour:** all lines are the same grey, except fainter into a match that has no players assigned yet.
- **"Enable Developer Analytics" is now "Enable Developer Console"** in Config, matching what it opens; the Developer Console is reached from the Console link in the bracket header (the version number it used to hang off was in the CAD box, which is gone). Help text updated to match.
- **In-app help for the Tournament page rewritten** for the new bracket: the header, moving around (drag, scroll or pinch, Fit all, hover), selecting a match (lines, Follow, Undo match, Match Controls), the colour legend, and running matches from Match Controls. The Config help mentions the new finals position setting.
- **Single elimination uses the new bracket view too**, with its own layout: rounds up to the semifinals, then the **Bronze final** level with the top semifinal and the **Final** midway between the semifinals. The semifinal winners fork straight after the semifinals and run under the Bronze final to the Final; a dashed line joins the Bronze final to that line, and "3rd place" sits under it. Rounds are labelled Round 1…, Quarterfinals, Semifinals. Everything else (cards, selection, Follow, Undo match, camera, header) is shared; the Finals toggle is hidden. The classic renderer has been removed (see below).
- **Results waiting are counted on the Match Controls button.** When a result arrives from a Chalker over the network, the header's Match Controls button shows an orange count ("1 result") until it is accepted there; the match's card shows RESULT. This replaces a "Result received" message that showed in the bottom-centre status area, which went with the CAD box.
- **A read-only tournament says so in the selection bar** ("Read only", on every match) instead of a message when undo was refused.
- **The classic bracket renderer is removed:** `js/bracket-lines.js` (about 1,900 lines) and the per-size card, layout, title and line functions, the old zoom/pan and default views, the CAD box updater (`updateTournamentWatermark()`) and the bottom-centre status messages (`updateStatusCenter()`) — about 3,600 lines in all. `renderBracket()` now always hands over to the bracket view; `zoomIn()`, `zoomOut()` and `resetZoom()` remain as the header's entry points. The network layer's two status messages are now skipped (it already checked the function exists): the results count replaces one, and the other confirmed a send that happens in Match Controls.
- **Lane and referee dropdown code for the old bracket cards is removed** from `js/lane-management.js` and `js/bracket-rendering.js`: the bracket halves of `refreshAllLaneDropdowns()` / `refreshAllRefereeDropdowns()` (the Match Controls halves stay), `refreshLaneDropdown()` and `refreshRefereeDropdown()` (no callers left), the duplicate copies of the referee refresh functions (one copy now, in `lane-management.js`), and `updateMatchLaneWithValidation()`, which nothing called.
- **Fix: changing a lane in Match Controls now updates the lane shown on the match in the bracket.** `updateMatchLane()` deliberately skipped redrawing the bracket, because the old card had its own lane dropdown that was refreshed instead; the new card shows the lane as a label, so it needs the redraw.
- **Fix: refusing a referee in Match Controls now resets Match Controls' dropdown.** It used to reset only the old bracket card's dropdown, so Match Controls kept showing the refused name until it redrew.
- **The classic bracket CSS is removed** from `css/styles.css` (about 650 lines: classic cards, floating buttons, CAD box, watermarks, the unused `glow` animation). The bracket page's frame rules (`.bracket-container`, `.bracket-viewport`, `.bracket-canvas`) now live only in `css/bracket-view.css`, set once instead of overridden. `css/styles.new.css` (3,238 lines), which no page loaded, is deleted.

  Files changed: `js/bracket-view.js` (new), `css/bracket-view.css` (new), `js/bracket-rendering.js` (`renderBracket()`, `renderCleanBracket()` hand double elimination to the new view; zoom, pan and drag functions defer to it), `tournament.html` (bracket page header replaces the floating controls and watermarks; new stylesheet and script), `js/tournament-management.js` (`updateClock()` also feeds the bracket header clock), `js/main.js` (`showPage()` refreshes the bracket header on entry; old bracket controls no longer initialised), `js/bracket-lines.js` (removed), `css/styles.css` (classic bracket rules removed), `css/styles.new.css` (removed, unused), `js/lane-management.js` (old card dropdown code removed), `js/clean-match-progression.js` (`updateMatchLane()` redraws the bracket), `js/dynamic-help-system.js` and `js/analytics.js` (Developer Console wording), `js/results-config.js` (finals position setting), `Docs/BRACKET-REDESIGN.md`

### Global Settings redesigned

- **The Config page is rebuilt in the bracket's design language.** Settings are grouped by what you're doing (Club, Tournaments, Match day, Bracket, Chalker, Server & backup, Developer), with a section list on the left and one setting per row: what it does on the left, the control on the right. On/off settings are switches; small choices are segmented buttons (Game 301 | 501 | 701 | Other, Handover QR code | Network | None); match lengths are − Bo3 + steppers in a Double / Single elimination grid; points are a Placing table and an Achievements table; lanes not in use are clickable lane numbers instead of a comma-separated list; the finals position shows small pictures of the two layouts. Labels and descriptions are rewritten in plain language.
- **One save for the whole page.** The seven Save buttons and their "saved" alerts are gone. Any change brings up a bar, "N unsaved changes · Discard · Save changes", and the section list marks the sections that changed. Reset to defaults fills in the defaults and leaves saving to the bar. An empty club name or an out-of-range starting score is refused in the bar, and nothing is saved.
- **Leaving with unsaved changes asks first:** Stay, Discard and continue, or Save and continue. Closing or reloading the tab gets the browser's own warning.
- **Stored settings are unchanged**, and every input keeps its id: the steppers and buttons write into hidden fields with the original ids, so `applyConfigToUI()` and the save functions work as before. Each save function takes `{ silent: true }`, and `saveAllSettings()` runs them all.
- **Fix:** saving lane settings no longer switches off "require a lane before starting", which has no control on the page and used to be reset to off on every save.
- **Shared design tokens:** the colours and fonts of the new design language now live in `css/design-tokens.css`, used by both the bracket and Global Settings.
- In-app help for Global Settings rewritten for the new page.

  Files changed: `css/design-tokens.css` (new), `css/config-page.css` (new), `js/config-page.js` (new), `tournament.html` (Global Settings markup, stylesheets, script), `js/results-config.js` (silent saves, `saveAllSettings()`, resets fill the form only, format cards), `js/main.js` (`showPage()` asks before leaving unsaved settings and fills the form on entry), `css/styles.css` (old Config styles removed), `css/bracket-view.css` (tokens from `design-tokens.css`), `js/dynamic-help-system.js`

## **v5.1.9** — Every Chalker Needs an Eraser (2026-09-27)

### Analytics: achievement corrections (Docker only)

- **A tournament's achievements can now be corrected after the fact, from Analytics.** Under `?tm`, each tournament in Register → Tournaments has an **Edit** button. Pick a player and add or remove 180s, tons, lollipops, high outs and short legs; **Reset Player** goes back to what was recorded. The modal shows the recorded values alongside, so it's always clear what has been changed.

  Correcting a finished tournament had turned out to be nearly impossible. Achievements reach Analytics as a one-time snapshot: each browser pulls a tournament from disk into its own register once and never reads it again. Editing the file, deleting and re-uploading it, or editing an exported register all failed somewhere along that chain — often in a browser nobody was looking at.

  Corrections go around the chain instead of through it. They are stored on the server as the *difference* from what was recorded, one record per player per tournament, and applied every time Analytics loads its tournaments. The recorded tournament and each browser's register are never modified, so the upload → pull flow is unchanged, and a tournament that is deleted and pulled again keeps its corrections. Because they are applied at the one point every view loads from, the Dashboard, Leaderboard and Players views, the tournament points and both point modes all include them. Match-level views keep showing what was recorded on the night.

  Counts can't go below zero, and high outs and short legs use the same ranges as the tournament's own statistics editor (101–170, 9–21 darts). Corrections are always applied and carry no marker. There is no reason field — the Edit modal is the record.

  Docker only: without the server there is nowhere shared to keep them, so the button only appears when the corrections API answers, and never on the public Analytics page.

  Files changed: `api/corrections.php` (new), `js/newton-history.js` (`_loadAllTournaments()` applies corrections; new corrections section and Edit action), `tournament.html` (Correct achievements modal), `docker/entrypoint.sh` (creates `tournaments/corrections/` owned by www-data), `js/dynamic-help-system.js`, `api/README.md`, `DOCKER-QUICKSTART.md`

### Fixes

- **Undo now clears lane and referee assignments on downstream matches that hadn't started.** If the winner or loser of a match had been placed in a next-round match, and that match already had a lane or referee assigned, undoing the first match left those assignments in place. The referee stayed marked as assigned and the lane as in use, until someone found the match and cleared them by hand.

  Undo already removed those assignment transactions from history; it just didn't clear the same fields on the live match objects, so the two disagreed. They are now cleared together. (A live or completed downstream match blocks undo in the first place, so only matches still waiting for players are affected.)

  Files changed: `js/bracket-rendering.js` (`undoManualTransaction()`)

## **v5.1.8** — Off the Wire (2026-09-13)

### Fixes

- **The welcome help shown on a first run works again.** Opening the app with nothing stored — a fresh install, or a new browser — pops a first-time guide after a couple of seconds. It was displaying "Overview: undefined" and none of its actual content. The same applied to the "Common Issues" button inside the help window: that content had never been able to render either.

  The cause was a shape mismatch. Every help topic is stored as a title, an overview and a set of sections, and the renderer expects exactly that. The scenarios topic — which holds both the welcome guide and the common-issues list — had instead been written with its two entries at the top level, so the renderer found no title, no overview, and no sections to draw. It now has the same shape as every other topic, and both are reachable.

  It went unnoticed because until recently something else was covering it. The help system used to watch the whole page for changes, and any change at all — including the clock ticking over — made it redraw itself with the current page's help. The broken welcome was replaced within seconds of appearing. That watcher was removed in v5.1.6 as wasteful, which it was; it was also, accidentally, hiding this.

- **The Handover setting now says that Network needs Docker.** It is listed as "Network (experimental, Docker only)", and the description explains why: the feature needs the small web server that ships with the container, so it does nothing if you opened `tournament.html` directly or are using the hosted version.

### In-app help caught up with Analytics

- **The built-in help now explains the Analytics page.** It had been describing only the Register — the tournament and match records — because that is all Analytics was when the help was written. Everything added since, which is most of what people actually use it for, went undocumented: the Dashboard, the Leaderboard and what its columns mean, the Players view, and the two controls that change every number on the page.
- **The Lens is explained properly**, including the part that is easy to miss: you set it under Register → Tournaments, but it governs the Dashboard, Leaderboard and Players as well. The help now says so, and points at the indicator in the page header as the first thing to check when a figure looks wrong.
- **Point modes and layers are explained** — Original scores each tournament with the values in force on the night, Current rescores everything with today's values, and the Ranking and Attendance toggles switch whole categories of points on and off. With the reassurance that none of it changes a stored result.
- **Automatic backup has help for the first time.** Uploading a completed tournament to the server without anyone remembering to do it is one of the better reasons to run the container, and it was not mentioned anywhere in the app.

### Analytics: the maintenance URL can be password-protected

- **`NEWTON_TM_PASSWORD` gates the `?tm` maintenance URL.** An analytics-only instance hides tournament management, and appending `?tm` brings the full Tournament Manager back for that browser — which is how a shared instance gets maintained. It was unauthenticated, so anyone who knew the parameter could use it. Setting the new environment variable requires `?tm=<password>` instead.

  **A wrong or missing password loads the ordinary Analytics page** — no error, no prompt, no hint that a maintenance mode exists at all. A login prompt would announce the door and invite a try; this way nothing is advertised.

  Leave the variable unset and nothing changes: bare `?tm` keeps working exactly as before, so no existing deployment is affected.

  Worth being plain about the limits: this is one shared password in an environment variable, not user accounts, and it appears in the server's access log and in browser history. It stops a curious club member. It is not protection against someone determined, and an instance reachable beyond people you broadly trust still wants a password at the web server.

### Docker: tournament storage was not writable on Linux

- **Uploading a tournament and network match handover both failed with a server error on Linux deployments.** The container writes both into `tournaments/`, which the documented compose file supplies as a bind mount. When that host directory does not already exist, Docker creates it as **root** — and the web server runs as `www-data`, which then cannot write to it. Every upload and every match transfer returned a 500. Stack managers such as Dockge make this the default case, since they create the stack directory themselves.

  It hid well: macOS and Windows Docker use a file-sharing layer that ignores the ownership mismatch, so the same setup works there. Only real Linux deployments were affected, and only if anyone tried to use the features.

  The container now checks at startup whether the web server can actually write to `tournaments/` and corrects the ownership if not, logging that it did so. A directory that is already set up correctly is left untouched. If the correction is impossible, it now says so loudly in the log with the command to fix it, rather than failing silently at request time hours later.
- **Network handover state directory.** `tournaments/network/` is likewise created at startup, as root and after the volume is mounted — the only moment both are true. The endpoints still create it themselves when missing, so non-Docker deployments are unaffected.
- **A failed transfer now tells you what went wrong.** The endpoints have always answered with a readable explanation — `{"ok":false,"error":"..."}` — even on a 500, and both clients were throwing it away and reporting a bare failure. The Tournament Manager now shows the server's own words, and the Chalker logs them. The directory error also names the exact path and the user it was running as, which is the part that depends on the deployment rather than the code.
- **Verified against a real container**, not a stub: the image was built, the clients were driven against it, and a match was dispatched to a lane, collected, scored, returned, reviewed and cleared. The failure mode was reproduced first, then fixed, then re-tested.

### Files changed

- `js/dynamic-help-system.js` — the `scenarios` help topic now has the `title`/`overview`/`sections` shape the renderer requires, so the first-run guide and Common Issues both display; new Analytics sections (views, lens, points, leaderboard, players) and a Server & Automatic Backup section under Config
- `tournament.html` — Handover's Network option labelled "Docker only" with an explanation; `?tm` honours `NEWTON_TM_PASSWORD` when set (constant-time comparison, silent fallback to the configured mode)
- `DOCKER-QUICKSTART.md`, `docker/docker-compose.yml` — the new variable, and a section on maintaining an analytics-only instance
- `docker/entrypoint.sh` — ensures `tournaments/` is writable by the web server (correcting ownership only when it is not), creates and chowns `tournaments/network/`, and warns in the log with a fix command if either is impossible
- `licensed/api/v1/_common.php` — directory and write errors name the resolved path and effective uid
- `licensed/js/network-tm.js` — `post()` preserves the server's error text; dispatch failures show it
- `licensed/js/network-chalker.js` — `post()` logs the server's error text instead of discarding it

---

## **v5.1.8-beta.1** — Behind the Bar (2026-09-07)

### Analytics safety

- **Export Register, Import Register and Import Tournament are hidden in analytics-only mode.** A shared Analytics instance is now something a whole club can browse, and one of those buttons — Import Register — replaces the stored match history for everyone who uses it. They were sitting in plain sight at the top of the page, next to the scope selector, with nothing but a file picker between a curious member and everybody's season. They are maintenance tools and they now behave like it: absent from the shared view, and still available to the maintainer on the same instance via the `?tm` escape hatch, which forces full mode. Deleting a tournament was already gated behind a separate server setting and is unchanged. Nothing is removed from the normal Tournament Manager — this affects the analytics-only deployment (`NEWTON_MODE=analytics`) only.
- No new setting was needed: `?tm` already forces full mode and is already the maintainer's route into a shared instance. **To be clear about what this is and is not** — `?tm` is not authenticated, so anyone who knows the parameter can still reach these tools. This removes the accident, not the possibility. Gating `?tm` itself is a separate job.

### Known issue

- **Network handover still does not work.** Transferring a match to a lane returns HTTP 500. The endpoints in `licensed/api/v1/` have never been parsed by a PHP interpreter — run `php -l` over them first. The feature remains off by default.

### Files changed

- `tournament.html` — the three maintenance controls carry `analytics-maintenance-tool`
- `css/styles.css` — `.mode-analytics .analytics-maintenance-tool { display: none; }`

---

## **v5.1.7** — Know Your Format (2026-09-07)

### Known issue

- **Network handover does not work.** Transferring a match to a lane returns HTTP 500. The endpoints in `licensed/api/v1/` have never been parsed by a PHP interpreter — run `php -l` over them first. The feature remains off by default (Handover: QR code) and nothing else in this release depends on it. Sharing a version number with `v5.1.7-beta.1` does not mean that beta has graduated.

### Tournament formats

- **You can hide the formats your club never plays.** Under Config → User Interface, "Tournament formats to offer" controls which formats appear on the Shuffle & Draw screen. A club that only plays double elimination can hide single elimination, and the wrong one can no longer be picked by accident on tournament night — which is exactly how a season opener ended up briefly in the wrong format, despite the confirmation dialog naming it twice. Preventing the choice beats warning about it.
  - **Existing tournaments are untouched.** Hiding a format only affects the screen where a new tournament is started. A tournament already created in a hidden format still opens, renders, undoes and exports exactly as before.
  - **At least one format always remains.** The last ticked box cannot be unticked.
  - **Formats added in future releases appear by default**, rather than staying invisible until found in Settings. The setting records what you have hidden, not what you have chosen — so a new format shows up for everyone, and clubs who want it gone can hide it too.
- **One list defines the formats.** Their names, descriptions and player limits were written into the tournament screen's markup; the Config checkboxes would have been a second copy of the same list. Both are now built from a single definition, so the two cannot disagree about which formats exist. It covers presentation only — a new format still needs its own progression tables and rendering, which stay where they belong.

### Fixes

- **The Status Panel shows the real player and match counts again.** During a live 14-player tournament the panel reported 9 players and 0 matches. It had been reading the tournament object's own `players`/`matches` arrays, which look authoritative but are never actually updated: they are created empty with the tournament and only ever re-pointed at the live arrays when a tournament is loaded from storage. Three ordinary actions break that link for good — generating a bracket, removing a player, and creating a tournament — after which those copies are frozen at whatever they last held, while the real roster and bracket carry on without them. Everything else in the app, including everything saved to disk, uses the live arrays; only the panel did not. It does now. This is a regression introduced in v5.1.6, where the panel was changed to read the in-memory tournament instead of re-reading storage on every save — the storage record was being written from the live arrays, so it had always been correct. A reload was enough to make the panel right again, which is why it was hard to catch.
- **A knocked-out player is struck through only in the match that eliminated them.** The same frozen copy was consulted when deciding where to show the strikethrough, so the check found no matches to compare against and marked every match a player had lost. In double elimination that meant a player who lost once on the frontside and again on the backside appeared eliminated in both. Display only — no scores, standings or stored data were affected.

---

## **v5.1.7-beta.1** — Look, No Camera (2026-09-06)

### Network handover (experimental)

Matches can be sent to a Chalker across the local network instead of being carried by QR code. **Experimental, and not yet tested on real hardware.** Nothing changes unless you choose it: the new setting defaults to QR, and with the default selected the app behaves exactly as it did.

The network code lives in `licensed/`, which is **not open source** — see `licensed/LICENSE.md`. It is free to use, with no key and no subscription, under a grant that is revocable for future releases. Everything else in the project remains BSD 3-Clause, and both apps work fully with that directory deleted.

- **New Config setting: Handover — QR code / Network / None.** QR is the default and unchanged. Network hides every QR affordance and offers a Transfer button on a started match in Match Controls. None hides both, for clubs entering results by hand. Configs saved before this setting existed default to QR.
- **Sending a match.** Assign the match a lane, start it, and press Transfer. It goes to whichever Chalker is serving that lane. Nothing else changes about how you run the tournament.
- **Receiving a result.** When a Chalker finishes, the match card shows a result is waiting. **It is never applied on its own** — you accept it, and accepting opens the same review you already know from scanning a result QR, lollipop counters and all. A result that is not accepted stays waiting, so there is nothing to lose by leaving it.
- **Fixing a wrong result.** Corrections are made on the Chalker, which owns the scoring and can undo back across legs, and the corrected result replaces the waiting one. The Tournament Manager never edits machine-recorded scoring — a record either comes from the Chalker or is declared by hand, never a mix of the two.
- **Why polling rather than a live connection.** A phone sleeps, gets backgrounded, and roams between access points all evening. A connection has to survive all of that; a request that simply happens again in a few seconds does not. There is no connection to lose and nothing to reconnect.
- **No new dependencies and no broker.** The endpoints are plain PHP served by the container's existing web server, and the state lives in the volume it already mounts.

Groundwork that came with it: the Tournament Manager's assignment payload is now built independently of the QR code that used to render it, so one definition of a match assignment serves both ways of sending it. The Chalker's result side already worked that way. No change to what either produces — the payload was verified byte-identical to the previous code across every combination of lane, referee, missing settings and awkward names.

### Chalker

- **The install banner can be dismissed again, and Install is never a dead end.** Its two buttons were the only inline `onclick` handlers left anywhere in the Chalker — everything else is wired with `addEventListener` — which made them the only thing in the app a strict Content-Security-Policy would silently disable. Where that applied, the banner sat permanently across the bottom keypad row with both buttons inert. They are now wired like every other control. Two further faults in the same few lines: the banner markup sits *after* the scripts, so the buttons are not in the DOM when `chalker.js` runs (the listeners now wait for `DOMContentLoaded`), and `installApp()` returned silently when the browser's deferred install prompt was missing or stale — the button genuinely did nothing. It now closes the banner and remembers the dismissal instead.

### Files changed

- `licensed/` — new, separately licensed directory: `LICENSE.md`, `README.md`, `api/v1/{_common,assign,result,heartbeat,lanes}.php`, `js/network-chalker.js`, `js/network-tm.js`
- `LICENSE` — scope note naming the `licensed/` carve-out
- `README.md` — "Licensing split" section
- `js/qr-bridge.js` — new `buildAssignmentPayload(matchId)`, split out of `openMatchQR()`, which now derives its subtitle's referee name from `signed.ref`; `applyQRResult()` clears the network mailbox once a result has been applied
- `js/results-config.js` — new `config.chalker.handover` (default `'qr'`), `getChalkerHandover()`, the `transferMatchToDevice()` guard, and load/save wiring
- `js/bracket-rendering.js` — match card shows Result / QR / Transfer according to the handover setting and whether a result is waiting; bracket card marks a waiting result; Match Controls' scan button hidden outside QR mode
- `js/clean-match-progression.js` — winner dialog's Scan Results QR button hidden outside QR mode
- `chalker/js/chalker.js` — new `NewtonChalkerBridge`, the narrow surface the optional network client uses; install-banner listeners wired on `DOMContentLoaded`; `installApp()` no longer returns silently without a deferred prompt; new `hideInstallBanner()`
- `chalker/index.html` — install-banner buttons use ids instead of inline `onclick`; Network Mode modal no longer claims a licence is required; loads the optional network client
- `tournament.html` — Handover dropdown in the Config → Chalker section; loads the optional network client
- `css/styles.css` — styling for a result waiting to be accepted
- `Docs/NETWORK-LAYER.md` — rewritten for the LAN-only, REST-first proof of concept; the superseded design is retained as an appendix with a note on what it got wrong

### Not yet verified

The PHP endpoints have never been run — there is no PHP interpreter on the development machine — and neither client has run in a browser or over a real network. The clients are tested against stubbed endpoints, which verifies them against the intended contract rather than against the actual server. Run `php -l` over `licensed/api/v1/` and play one match end to end before relying on any of it.

## **v5.1.6** — One Throw, One Score (2026-09-06)

**Completes the code-improvement plan** (`Docs/CODE-IMPROVEMENT-PLAN.md`) — Phases 1–6 are now done. This release carries the end of Phase 4 (analytics bracket-preview isolation 4.3, NewtonMatchDB write-path atomicity 4.6; the remaining 4.10 schema-version item was reviewed and declined, see below) and the whole of Phase 6 — performance (6.1–6.5) and consolidation (6.7–6.21).

No new features. The theme is doing the same work once instead of repeatedly: `completeMatch()` persists once rather than 2–4 times (127 → 63 saves across a full 32-player tournament), `renderBracket()` parses the transaction history once rather than per card (~63 → 1), Analytics fetches match lists concurrently and caches them (48 → 12 reads; point-mode/layer toggles 48 → 0), and a whole-body MutationObserver is gone. Every change was verified by differential testing against the pre-change file — full tournaments replayed in both formats at every bracket size, state compared after each completion — with the details recorded per item in the plan doc.

Visible changes: the 16-player bracket font fix, the Short Leg Threshold reaching the Chalker, F1 help matching the page after a reload, and Analytics reflecting corrections to a finalized tournament without a reload.

**Two documented declines**, both reopenable with evidence: 6.1 part B (`checkRefereeConflict`'s O(n²) scan — pure in-memory, unmeasured payoff) and the `renderLeaderboard` split inside 6.11 (measured overlap was ~20 lines of a 292-line function; the plan's own rule is "split when touched, not as a dedicated pass").

### Analytics / History

- **Previewing a bracket no longer forgets the tournament you were editing.** In analytics-only mode (`NEWTON_MODE=analytics`), the "Bracket" preview writes the fetched tournament into the shared `currentTournament` pointer in order to render it. Two problems followed — both only reachable when the analytics box is *also* used to edit completed tournaments through the `?tm` escape hatch (same origin, shared localStorage):
  - The "back to Analytics" button did `localStorage.removeItem('currentTournament')`, silently deactivating whatever tournament you had open. On the next `?tm` reload it was gone from the pointer — its data was safe in `dartsTournaments`, but you had to reopen it from Recent Tournaments.
  - Reloading *while previewing* was worse: `autoLoadCurrentTournament()` rebuilt the tournament object without the `_analyticsPreview` guard flag, so the next save persisted the read-only preview into `dartsTournaments` as a phantom "real" tournament.

  The fix keeps the preview read-only and self-contained. `viewBracket()` now stashes the real active tournament (if any) under a transient `_preAnalyticsPreviewTournament` key before overwriting the pointer — skipped when the prior value is itself a preview, so chaining preview→preview can't turn an intermediate preview into the stash. A new `exitAnalyticsBracketPreview()` restores that stash on exit (or clears the pointer when nothing was active, preserving prior behavior), and the Analytics back button now calls it instead of blindly deleting `currentTournament`. `autoLoadCurrentTournament()` now carries `_analyticsPreview` across a reload, so the no-persist guard survives. Net effect: a `?tm` edit session survives a preview round-trip *and* a mid-preview reload instead of being booted out — the fix improves the edit workflow rather than merely protecting it.

### Match register (NewtonMatchDB)

Three IndexedDB write paths were made atomic and faster. None change the public API or any caller.

- **Re-completing a match can't lose the record to a race.** `saveMatch()` checked for an existing `(tournamentId, matchId)` record in one transaction and wrote in a *separate* one. Two near-simultaneous saves could both see "no existing" and both `add()`, the second colliding on the unique index and silently dropping the re-completion. The lookup and the write now share **one** transaction; because IndexedDB serializes overlapping read-write transactions, the second save sees the first's record and updates it in place.
- **Deleting a tournament is all-or-nothing.** `deleteTournament()` opened a fresh transaction per match in a loop, so a mid-loop failure could leave a half-deleted tournament (some matches gone, meta still present). It now deletes every match (via the `tournamentId` index cursor) and the tournament-meta record in a **single** transaction that aborts as a whole on any failure.
- **Importing a register dump is one transaction, not two per match.** `importAll()` did a lookup and a write in separate transactions for *each* match record — slow on large dumps and non-atomic. It now processes the whole match batch in one transaction, and de-dupes the incoming records by `(tournamentId, matchId)` first so a malformed dump with a duplicate can't abort the batch on the unique index (well-formed exports never contain duplicates).

All three now reject on transaction abort so a failure surfaces to the caller instead of hanging.

### Bracket rendering (Phase 6 cleanup)

- **Fixed the "BS-FINAL" label font on 16-player brackets.** The indicator's font-family was misspelled `'Ïnter'` (with a diacritic) in the 16-player code path, so that label silently fell back to the default sans-serif instead of Inter. Fixed while de-duplicating.
- **Consolidated duplicated bracket-line code.** The three byte-identical "BS-FINAL indicator" builders (8/16/32-player) are now one `createBSFinalIndicator`, and the three identical nested `createLoserFeedLine` copies are now one shared module-level helper (with a z-index parameter for the 32-player layout, which stacks its feed lines above the straight progression lines). About 199 fewer lines — and the font typo can no longer silently reappear in one copy.
- **Removed bracket-geometry debug logging.** Deleted ~27 developer `console.log`s that narrated internal layout math on every bracket render (coordinate dumps, `🔧 Creating…`, `🎯 Rendering…`). Operational logging elsewhere — lifecycle, save/load confirmations, ranking calculation, and the `processAutoAdvancements` infinite-loop guards — is intentionally kept as observability (there is no automated test suite), and every `console.warn`/`console.error` is untouched.
- **One source for the default bracket view.** The per-format, per-size default zoom + pan values were duplicated in two places (the initial render in `renderBracket()` and the Reset-zoom button in `resetZoom()`), and only one copy carried the "needs tuning" notes — a drift waiting to happen. Extracted `getDefaultView(format, bracketSize)`; both call sites use it. No behavior change (the two copies held identical values).
- **32-player backside background uses the shared builder.** The faint gradient box behind the backside bracket was drawn via a shared `createBacksideBackground()` for 8- and 16-player brackets, but the 32-player renderer inlined its own copy of the same logic. It now calls the shared function (which already had a matching 32-player branch) like the other sizes. No visual change.
- **32-player backside placement labels follow the rendered match positions.** For 8- and 16-player brackets, the elimination-place labels above the backside bracket read the match Y positions from the render pass; the 32-player labels instead recomputed those positions with a parallel copy of the geometry — correct today, but liable to silently drift out of alignment if the 32-player layout ever changed. The 32-player renderer now records each round's first-match Y as it draws it, and the labels read those values — so labels can no longer desync from the matches. No visual change (per-round label positions verified numerically identical).

### Tournament export (Phase 6 cleanup)

- **De-duplicated the export path and fixed a blob-URL leak.** `exportTournament()` was re-assembling the entire export payload (history load, pruning, player-list snapshot, the full object shape) instead of using the shared `buildTournamentPayload()` that the server-upload paths already use — so the two could silently drift. It now calls the shared builder, and it releases the download's object URL with `URL.revokeObjectURL()` (previously the blob was retained for the page's lifetime on every export).
- **Export filenames are sanitized.** Filesystem-hostile characters in a tournament name (`< > : " / \ | ? *` and control characters) are now replaced with `-` when building the filename, so a name like `Spring/Fall: Cup` produces a valid download instead of a broken or path-traversing one. Sanitization is deterministic (re-uploading the same tournament still overwrites its file) and normal names are unchanged.

### Analytics internals (Phase 6 cleanup)

- **One source for the achievement-points formula.** The calculation (180s + tons + high outs + short legs, each times its configured point value) was copy-pasted in five places across the Leaderboard, the tournament-points computation, the Matches tab, and the match-detail views. It's now a single `_achPoints(stats, p)` helper. No behavior change — the numbers are identical — but a future change to the point rules (or a new achievement type) is now a one-line edit instead of five that can drift apart.
- **One source for decoding Chalker visit scores.** The base64 decoding of a leg's per-visit scores was inlined twice in the analytics views (the three-dart-average aggregation and the match-detail leg table) instead of using the existing `NewtonStats.decodeVisits()`. Both now call it, so the encoded-visit format is defined in exactly one place. No behavior change (round-trip decode verified identical).

### Performance (Phase 6)

- **One definition of how player names are matched.** Analytics identifies players by name — each tournament stores its own player ids, so there is nothing stable to join on across tournaments. That means every view has to agree on exactly how a name is tidied up before comparing it, or the same person can appear twice, or a player's record can differ between two screens. That tidying was written out by hand in twenty places. It is now defined once. Alongside it, the Leaderboard and the Players tab had identical copies of the match win/loss count — they now share one, so those two screens cannot drift into reporting different records for the same player. No change to any figure on screen.

- **Completing a match saves once instead of two to four times.** Recording a result touched the saved tournament repeatedly in a row: once for the result, again after recalculating live rankings, again if the match set a placement (the Backside Final's 3rd place, or a Single Elimination bronze match's 3rd/4th), and again if it finished the tournament. Each of those rewrote the entire tournament — every player, every match — and redrew the status panel and results table with it. It all happens in one go now: the result, the rankings and any placement are worked out first, then written once. Playing a full 32-player double-elimination tournament went from 127 saves to 63. Nothing about the results changes — verified by replaying whole tournaments in both formats at every bracket size, including byes, and comparing the complete state after every single match.
- **Removed a dead "reopen Match Controls" path.** After a completion, the code could schedule the Match Controls panel to reopen half a second later, on top of the refresh it had already scheduled — but the flag that triggered it was never switched on anywhere in the app, so the branch could never run. Deleted, along with the two leftover places that reset the unused flag.

- **The help system stopped watching the entire page for changes.** To notice which page you were on, it ran a MutationObserver over the whole document — every element, every class attribute — so it woke up on *every* DOM change the app made: each bracket render, each clock tick, each table repaint, and asked "which page is active?" each time. The app already knows when it changes page, so it now simply says so. No observer remains in the codebase.
- **F1 help matches the page you're on after a reload.** The app restores your last page when it loads, but the old page-detection was only installed half a second later and never saw that first transition — so reloading straight onto, say, the Tournament page and pressing F1 gave you Setup help until you navigated somewhere else. Fixed as a side effect of the change above.

- **Analytics loads its data in parallel, and stops reloading it when nothing changed.** The Dashboard, Leaderboard, Players and Matches views each walked the finalized tournaments one at a time, waiting for one tournament's matches to come back from the database before asking for the next. They now request them all at once. On top of that, the loaded matches are kept for reuse: flipping the point mode (Original/Current) or toggling the Ranking and Attendance layers only changes how the numbers are *multiplied*, not which matches exist — but every toggle used to re-read everything from the database anyway. Those toggles now do no database reads at all. With a dozen tournaments loaded, rendering the four views drops from 48 reads to 12, and a toggle drops from 48 to none. Same numbers on screen, noticeably less waiting.
- **Analytics notices when you edit a finished tournament.** Analytics keeps the finalized tournaments it has loaded in memory, but nothing told it to let go of them when a match in one of those tournaments was undone or re-entered — something possible through the maintenance workflow for correcting a completed tournament. Analytics could therefore keep showing pre-edit results until the page was reloaded. Completing and undoing a match now clear that cached copy once the change has been written, so returning to Analytics shows the corrected data.

- **Drawing the bracket reads the match history once instead of once per match.** Every match card asks whether its result can still be undone, and that check re-read and re-parsed the tournament's entire transaction log out of localStorage — about 63 full parses per redraw on a 32-player bracket, repeated after every lane assignment, referee change and completed match. The history is now parsed a single time per redraw and shared by all the cards. Same undo indicators, same behavior; the bracket just stops doing the same expensive work sixty-odd times in a row.

- **The status panel no longer re-parses the whole tournament on every save.** `updateTournamentWatermark()` runs after each `saveTournament()` — and it began by re-reading the `currentTournament` record back out of localStorage and `JSON.parse`-ing the entire tournament, purely to display a name, a few counts and a status. It now reads the in-memory tournament object, which is the same data: every writer of that localStorage record serializes it *from* the in-memory object, so the stored copy can never be newer. One full parse of the tournament blob is gone from every save (and every bracket render), and the panel is now consistent with the header's Active Tournament display, which already read the in-memory object. No visible change.

### Undo internals (Phase 6 cleanup)

- **One source for "what's blocking this undo?".** Two separate places worked out whether a completed match could still be undone: one answering yes/no (which decides whether a winner's name is clickable to undo it), the other producing the wording you see on hover and in Match Controls — *"Cannot Undo, blocked by FS-3-1 (live)"*. Both walked the progression table the same way, and they had to be kept in step by hand: when QR completions were added, the same fix had to be made twice, and it was missed in one of them (fixed in v5.1.5). They now share a single `getUndoBlockingMatches()` that returns the blocking matches; one caller checks whether the list is empty, the other turns it into the sentence. Each keeps its own separate rules — read-only tournaments, walkovers, and matches with no undoable result — since those differ. No behavior change: the two implementations were verified equivalent before the merge, and the merged version was checked against the old one across 1,200 randomized tournament states in both formats at every bracket size (32,400 comparisons, no differences).

### Dead code removal (Phase 6)

- **Removed an unused duplicate of the excluded-lanes parser.** `parseExcludedLanes()` in `lane-management.js` was byte-identical to `parseExcludedLanesString()` in `results-config.js` but had no callers anywhere in the codebase. Deleted it; the live `parseExcludedLanesString()` (with its one caller) is unchanged.
- **One source for the developer-mode check.** The same "is developer mode on?" block was inlined three times (twice in the watermark render, once in the late-registration info modal), and each re-parsed the `dartsConfig` localStorage value instead of reading the loaded in-memory config. Extracted a single `isDeveloperMode()` that reads `config.ui.developerMode` — one definition, and it honors the single-source-of-truth principle.
- **One source for the results-table player sort.** The "paid players, ranked first then alphabetical" sort was copy-pasted in three results functions (the results table, the JSON export, and the CSV export). Extracted `getSortedPaidPlayers()`; all three call it. Identical ordering, no behavior change.
- **Referee assign/clear share one path.** Assigning a referee and clearing one duplicated ~45 lines of near-identical follow-up work (record the change, save, re-render, refresh Match Controls). They're one shared tail now, with each case only setting the referee and its history description. Also removed a redundant dropdown refresh that the bracket re-render already covers.

### Chalker

- **The Short Leg Threshold setting now actually reaches the Chalker.** The Config page's "Short Leg Threshold (darts)" lives under the CHALKER section, but until now the Chalker never received it — it judged short legs with its own built-in table while the Tournament Manager awarded the achievement using the configured value, so the on-screen "Short Leg" badge could disagree with what you were actually credited. The threshold is now sent to the Chalker with the rest of the match setup (alongside x01 format and max rounds), so the badge and the award always agree. Standalone Chalker matches (started on the device, not assigned from a tournament) keep using the sensible built-in table, and older QR codes without the new field fall back to it too.

### Design decision — additive-only localStorage schema (4.10 declined)

Item 4.10 proposed adding a `schemaVersion` marker to the localStorage records. Declined: the stored schema is **additive-only** (every change so far adds an optional field with a sensible default when absent), and additive changes are handled by idempotent "missing → default" adaptation at read time, which is skip-proof regardless of how many versions a record predates — so no version marker or migration ladder is needed. A marker only pays off for a *destructive/ambiguous* change (rename a field; change a value's meaning), which the app avoids. This is now a standing principle in `CLAUDE.md` (Development Principles → Data Integrity). Export files keep `exportVersion` because they arrive from any vintage; localStorage evolves in lockstep with the app and does not.

### Files changed

- `js/clean-match-progression.js` — Phase 6: `completeMatch()` persists once at the end of the success path instead of 2–4 times (6.2), with the `isTournamentFinal` computation moved inside the following `try` so a throw can't skip the save; dead `window.commandCenterWasOpen` reopen branch + both clear-the-flag blocks removed (6.2); NewtonDB match + meta writes now invalidate the Analytics cache once both land (6.5)
- `js/newton-history.js` — Phase 6: new `_playerKey(name)` (single definition of the player-name normalization, replacing 20 inline copies) + `_tallyMatchWinLoss(matches, playerMap)` shared by the Leaderboard and Players tab (6.11); `viewBracket()` stashes the prior real `currentTournament` into `_preAnalyticsPreviewTournament` before overwriting (skips re-stashing when the prior is already a preview); Phase 6: new `_matchesByTournament` cache + `_loadMatchesFor()` (`Promise.all`, input order preserved) replacing the four sequential `for … await` match fetches in `renderDashboard`/`renderPlayersTab`/`renderLeaderboard`/`renderAllMatches`, cleared by `_invalidateCache()` (6.5); extracted `_achPoints(stats, p)` helper, replacing 5 duplicated achievement-points formulas; replaced 2 inline base64 visit decodes with `NewtonStats.decodeVisits()`
- `js/main.js` — Phase 6: `showPage()` now calls `setHelpPage(pageId)` (6.4); new `exitAnalyticsBracketPreview()` (stash-restore on preview exit); `autoLoadCurrentTournament()` now copies `_analyticsPreview` so the no-persist guard survives a reload
- `tournament.html` — Analytics back button calls `exitAnalyticsBracketPreview()` instead of `localStorage.removeItem('currentTournament')`; Phase 6: Config CHALKER blurb now notes the short-leg threshold is sent
- `js/newton-db.js` — `saveMatch` single-transaction upsert (TOCTOU fix); `deleteTournament` single cross-store transaction with cursor delete; `importAll` single-transaction batched match upsert with `(tournamentId, matchId)` de-dupe; all three reject on `tx.onabort`
- `js/bracket-lines.js` — Phase 6: three BS-FINAL indicators → one `createBSFinalIndicator`; three nested `createLoserFeedLine` → one module-level helper (z-index parameter); `'Ïnter'`→`'Inter'` font fix; removed 20 bracket-geometry debug logs
- `js/bracket-rendering.js` — Phase 6: undo's `NewtonDB.deleteMatch` calls now invalidate the Analytics cache once the deletes land (6.5); new `getUndoBlockingMatches(matchId, history)` helper, replacing the duplicated downstream-blocking scan in `isMatchUndoable()` and `getDetailedMatchState()` (6.8); new render-pass `_renderPassHistory` memo (primed in `renderBracket()`, cleared in a `finally`) so `isMatchUndoable()` reuses one parsed history per redraw instead of re-reading localStorage per card (6.1 part A); removed the 6 per-render `🎯 Rendering N-player` logs and 1 stray `populateRefereeSuggestions` log; extracted `getDefaultView()` replacing the duplicated zoom/pan defaults in `renderBracket()`/`resetZoom()`; `render32PlayerBacksideMatches` now calls the shared `createBacksideBackground()` instead of an inline copy, and captures each backside round's first-match Y into `positions` for the placement labels; `createBacksidePlacementLabels` case 32 reads those instead of recomputing; `updateMatchReferee` clear/assign paths merged into one shared tail (redundant `refreshAllRefereeDropdowns()` calls dropped)
- `js/tournament-management.js` — Phase 6: `exportTournament()` now reuses `buildTournamentPayload()` (removes a duplicated payload builder) + adds `URL.revokeObjectURL`; filename sanitized at source in `buildTournamentPayload()`
- `js/dynamic-help-system.js` — Phase 6 (6.4): whole-body MutationObserver removed; new `setHelpPage(pageId)` called from `showPage()`
- `js/lane-management.js` — Phase 6: removed the unused duplicate `parseExcludedLanes()`
- `js/results-config.js` — Phase 6: new `isDeveloperMode()` helper (reads in-memory `config`); new `getSortedPaidPlayers()` helper replacing 3 duplicated results sorts
- `js/player-management.js` — Phase 6: late-reg modal uses `isDeveloperMode()` instead of an inline localStorage-parsing IIFE
- `js/tournament-management.js` (watermark) — Phase 6: both developer-mode checks use `isDeveloperMode()` instead of inline IIFEs; `updateTournamentWatermark()` reads the in-memory `tournament` instead of re-reading and re-parsing `currentTournament` from localStorage (6.3), with JSDoc added
- `js/qr-bridge.js` — Phase 6 (6.20): assignment payload now carries `slt` (short-leg threshold); (6.21) note that the QR-detect helpers intentionally differ from the Chalker's
- `chalker/js/chalker.js` — Phase 6 (6.20): `getShortLegThreshold()` helper prefers the assigned threshold, falls back to the built-in table; reads `slt` from the QR payload; (6.21) divergence note
- `chalker/sw.js` — cache bumped to `chalker-v110`
- `chalker/index.html` — `chalker.js?v=13`
- `CLAUDE.md` — added the additive-only localStorage schema-evolution principle (Data Integrity)
- `Docs/CODE-IMPROVEMENT-PLAN.md` — Phase 4 complete (4.3/4.6 done, 4.2 corrected, 4.10 closed); Phase 6 in progress (6.1 part A, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 6.10, 6.11, 6.12–6.20 done; 6.21 closed; 6.1 part B + the renderLeaderboard split declined)

### Migration

No data migration required. The `_preAnalyticsPreviewTournament` key is transient — written and cleared within a preview session, and only ever read when exiting a preview; a value left behind by a hard-closed tab mid-preview is inert and gets reclaimed on the next preview exit. The NewtonMatchDB changes are behavioral only — the schema (stores, indexes, `DB_VERSION`) is unchanged.

---

## **v5.1.5** — Undo the Undone (2026-07-04)

A code-quality release. A full review of the codebase (four parallel deep reviews covering the bracket core, app/state layer, analytics/history/DB, and QR/Chalker/API — plan and findings in `Docs/CODE-IMPROVEMENT-PLAN.md`) produced a six-phase improvement plan. This release ships Phase 1 (correctness bug fixes, ~25 items), Phase 2 (REST API hardening), Phase 3 (the HTML-escaping sweep — XSS hardening plus the apostrophe-in-names bug), Phase 5 (dead code removal, ~950 lines), and most of Phase 4 (persistence & data-integrity hardening — guarded registry reads, storage-full handling, render-race tokens, and import crash-prevention). No new features; the only visible change beyond the fixes is a restyle of the Storage Space dialog.

### Transaction & undo integrity

- **Transaction IDs are now guaranteed unique.** All five mint sites previously used `` `tx_${Date.now()}` ``; `processAutoAdvancements()` completes walkover chains synchronously, so several transactions could share one ID within the same millisecond. Undo looks transactions up by ID — a duplicate could silently strip an *unrelated* match's transaction from history while its board state stayed completed. New shared helper `generateTransactionId()` (timestamp + monotonic counter) in `clean-match-progression.js`, used everywhere.
- **Undo gates now respect QR completions downstream.** The downstream-blocking checks in `isMatchUndoable()` and `getDetailedMatchState()` only blocked on `MANUAL` completions — undoing an upstream match would destroy a Chalker-recorded QR result (legs, averages, achievements) with no warning. All four checks now treat QR like MANUAL.
- **Undo cleans up NewtonDB for consequential matches.** `undoManualTransaction()` deleted the IndexedDB match record only for the target match; rolled-back downstream matches left orphaned records that Analytics kept counting. All rolled-back matches are now deleted from the register.
- **Undo confirmation modal fallback no longer bypasses confirmation.** If the modal elements were missing, the fallback executed the destructive undo directly; it now logs and aborts.
- **Winner-confirmation input listeners no longer leak.** `showWinnerConfirmation()` added anonymous input listeners on every open but removed a function that was never attached; handlers accumulated all session, each closing over a stale match. Named handler, stored on the modal, removed on cleanup and re-open.

### Transaction pruning (export + Developer Console)

- **Pruning matches strictly on `matchId`.** All four pruning sites (three in `analytics.js`, one in the export path) grouped transactions with a `description.includes(matchId)` fallback — `FS-1-1` is a substring of descriptions for `FS-1-10`…`FS-1-16`, so a completed match could claim (and prune) transactions belonging to other, including live, matches: lane/referee assignments and START_MATCH entries. The substring fallback is gone.
- **Transactions without an identifier are skipped** instead of collapsing to an `undefined` key that matched — and dropped — every other id-less transaction.

### Import / export

- **Saved Players are restored on import again.** Import wrote the snapshot to the legacy `savedPlayers` localStorage key; the app reads `playerList`. The feature was silently broken.
- **Old-format detection works on the overwrite path.** `window.isOldFormatImport` was only set by the confirm modal; re-importing over an existing tournament used a stale or undefined flag, which could restore incompatible pre-v4.0 snapshot history (or skip valid v4.0 history).
- **Removed a stale export that threw on every page load.** `window.generateResultsCSV = generateResultsCSV` referenced a deleted function — an uncaught ReferenceError that aborted the remaining `window.*` assignments in `results-config.js`.

### Analytics / History

- **Match detail points use the match's own tournament.** The Points column was computed from whichever tournament was last opened via the register (`_activeTournament`) — wrong `configSnapshot` in Original point mode when opening from the flat Matches tab. The detail builder now receives the match's own tournament record.
- **Cancelled imports no longer fire later.** The Analytics import confirm handler was only removed on click; dismissing the modal left it attached, and confirming a second file imported both. Stale handlers are now replaced on re-wire.
- **Tiebreak legs no longer corrupt three-dart averages.** Legs with `cd === 0` (tiebreak) were credited with a full 501 scored plus three invented checkout darts; they're now excluded, consistent with `extractAchievements()`.

### Chalker

- **Cross-leg undo restores both players' scores.** `restoreFromVisit()` only restored the undone player's score when stepping back into a previous leg; the opponent kept the new leg's starting score, corrupting subsequent bust/checkout validation. `undoLastVisit()` now recalculates both scores from the visit log.
- **Tiebreak and edited-checkout wins reach match history.** Only `completeLeg()` routed through `onMatchComplete()`; the other two completion paths skipped `saveMatchToHistory()`/`clearCurrentMatch()` entirely — no history entry, no Result QR from history, stale `current_match`. Both now route through `onMatchComplete()` (which also gives them the same stay-on-scoreboard behavior as normal wins).
- **Offline PWA actually boots offline.** The page loads `js/chalker.js?v=N` but the service worker cached the unversioned path, and `caches.match()` never matches query-stringed requests — the main script always hit the network. Fixed with `ignoreSearch: true`; the QR scanner libraries (`jsQR.js`, `qr-scanner.umd.min.js`, worker files) are now in `CACHE_FILES` so QR match assignment works offline; `cache.addAll` failures are logged instead of silently failing the install. Cache bumped to `chalker-v109`, script to `?v=12`.
- **Edit-mode "empty Enter deletes the last visit" is reachable** — `handleEnter()` returned early on an empty buffer before the delete branch could run.
- **Tiebreak cancel no longer strands input in an invisible row** — on a full board there is no new-entry cell, so cancel now enters edit mode on the last recorded visit. Undoing out of a tiebreak leg clears the `tiebreak` flag so a leg later won normally isn't misreported.
- **Rematch uses the active match config, not the form.** After a QR-assigned match the form held older manual values; rematch now rebuilds from `state.config`, stripping the QR identity fields (`matchId`/`tournamentId`/`serverId`) so a casual rematch isn't reported as the tournament match.

### Smaller fixes

- `requireLaneForStart` is actually enforced — it warned and continued; it now blocks the start *before* the state change (no stray START_MATCH transaction).
- `stats.shortLegs` initialized as an array at player creation (was `0`, patched lazily elsewhere); `snapshotPlayerStats()` guards legacy numeric values with `Array.isArray()` instead of throwing mid-`toggleActive`.
- Bo1 matches: loser legs max is `0`, not `2` (`match.legs - 1 || 2` falsy-zero bug).
- `advancePlayer()` returns `false` when a destination match lookup fails instead of reporting success.
- Watermark match count excludes walkovers via `isWalkoverMatch()` (the old `player1 !== 'BYE'` compared objects to a string — always true).
- QR result payloads are validated (`Array.isArray(payload.legs)`) *before* the scanner is torn down — a malformed payload previously left the user with no modal and no error.
- Club logo candidates load sequentially in priority order (parallel loading let the last-to-load win).
- `api/upload-tournament.php` reports `overwritten` correctly (`file_exists()` was checked after the write, so it was always true).
- `loadSpecificTournament()` no longer dereferences before its null check; `debugBracketGeneration()` returns after its guard.

### REST API hardening (Phase 2)

Robustness fixes only — the documented security model (no built-in auth; deployers protect exposed instances) is unchanged, and CORS behavior is untouched.

- **The API kill switch actually kills.** `NEWTON_API_ENABLED` was only honored as the exact string `false`; `FALSE`, `0`, `off`, and `no` (any case) now all disable the API. Unset still means enabled (opt-out model preserved).
- **Upload limits.** `upload-tournament.php` rejects payloads over 10 MB (413) before decoding, and requires the data to minimally look like a tournament export (`id`, `name`, `players` array) — no more arbitrary JSON hosted under `/tournaments/`.
- **Optional relay allowlist.** New `NEWTON_RELAY_ALLOWLIST` env var (comma-separated hostnames): when set, `relay.php` only forwards to those hosts; when unset, behavior is unchanged. Documented in DOCKER-QUICKSTART.md and commented examples in both compose files. `CURLOPT_FOLLOWLOCATION` deliberately left enabled — disabling it would break relaying through http→https redirects.

### HTML-escaping sweep (Phase 3)

Every user-, import-, and QR-derived string that reaches the DOM is now HTML-escaped, and identifiers are no longer passed through inline `onclick` strings. This closes a systemic pattern flagged independently by all four reviews, with three consequences:

- **It broke on legal names today.** A player named `O'Brien` produced a `SyntaxError` in generated handlers like `onclick="addPlayerFromList('O'Brien')"` — registration cards, history checkboxes, and delete buttons all failed on an apostrophe.
- **Stored XSS.** Imported tournament JSON and scanned QR payloads are barely validated, so a crafted player/tournament name (or QR field) could execute script in the app context.
- **HTML-escaping inside a JS string is a false fix.** Several sites wrapped names in an escaper *inside* an `onclick` string; the browser HTML-decodes the attribute before the JS parser sees it, so the escaping evaporated.

The fix, applied uniformly:

- **One canonical `escapeHtml()`** in `main.js` (window-exported), quote-safe so it is valid in both element text and quoted attributes. The Chalker, being a standalone app, carries an identical copy. The weaker `textContent`-based private copy in `analytics.js` (which didn't escape quotes) was removed.
- **Identifiers no longer travel in inline handler strings.** Handlers that carried a name or id were converted to either a per-render numeric-index lookup with one delegated listener, or `data-*` attributes (where HTML-escaping *is* correct, since the value is read via `getAttribute`, never evaluated). `NewtonTable` row clicks now dispatch by integer index into the data array — one change that hardened every table at once and removed its flawed `_escAttr` (which escaped `'` but not `"`). Analytics/History cell buttons use `data-*` attributes with a shared capture-phase delegated listener, so `stopPropagation` still suppresses the row click.

Coverage spans registration and player lists; tournament status, Recent Tournaments (including the server-supplied filename), import messages, and the bracket watermark; match cards, the command center, and referee dropdowns/suggestions across the bracket; the two bracket headers (club + tournament name); the Analytics tables and Developer Console; and the QR result preview, the QR Payload Inspector (which renders deliberately *unverified* scanned payloads), and the Chalker's match-assignment confirm, leg breakdown, and history.

One extra vector was caught during verification: the QR Inspector's JSON-parse-failure branch dumped the raw scanned string into `innerHTML` by string concatenation (missed by the initial template-interpolation sweep) — now escaped. Numeric-id inline handlers (e.g. `openStatsModal(${player.id})`) were left as-is by design; guaranteeing those ids are numeric against crafted imports belongs to a later persistence-validation pass.

### Dead code removal (Phase 5, ~950 lines)

Every deletion re-verified caller-free by grep at delete time (including `onclick` strings in HTML). Highlights:

- **The dead parallel undo implementation** (`undoTransaction`, `undoTransactions`, `clearPlayerFromDownstream`, `hasWalkoverPlayer`, plus orphaned `getUndoneTransactions`/`saveUndoneTransactions`/`clearTournamentCompletionState`) — depended on `beforeState`, which transactions no longer carry, so it *threw if invoked*, and it was window-exported: one console call away from corrupting a live tournament.
- **The dead redo system** (`getRedoableMatches`, `handleRedoClick`, commented UI block, the always-empty `${redoButton}` template slot) — same `beforeState` dependency.
- **`rebuildBracketFromHistory()`** (~185 lines, zero callers) — and corrected `undoManualTransaction()`'s JSDoc, which falsely claimed undo used it.
- The misleading singular `processAutoAdvancement()` (picked the *opposite* winner from the live plural version), `getDownstreamMatches()`, `handleBracketGenerationError()`, `showingAllTournaments`/`toggleTournamentView`, `safeSaveTournament()`, `updateLaneConfiguration()` + `saveConfiguration()` (and their window exports), the dead `config.lanes` bootstrap in lane-management.js, the duplicate window-export block in clean-match-progression.js (strict subset of the surviving one), `.cc-modal-content` scroll no-ops (selector matches nothing), unreachable bracket-size fallbacks, `zoomAnimationFrame`, and invisible placeholder/zero-sized line elements plus the dead `finalsX` parameter in bracket-lines.js.
- **One review finding was rejected during implementation:** `buildMatchSourcesLookup()` (analytics.js) has a live caller in `showMatchProgression`'s "Fed by" line — kept.

### Persistence & data-integrity hardening (Phase 4)

Phase 4's error-state fixes. Deferred to v5.1.6+ by choice: analytics-preview isolation (4.3 — reassessed as a low-priority analytics-only-mode edge), NewtonDB write atomicity (4.6), and the schema-version design item (4.10).

- **A corrupt tournaments registry no longer bricks the app.** New `readTournamentsRegistry()` helper parses `dartsTournaments` inside a try/catch and returns an empty array (with a one-time warning) instead of throwing — one bad value previously broke loading, deletion, and the Recent Tournaments list at once, with `loadRecentTournaments()` failing as a silent unhandled rejection that left the panel blank. Applied to the six read-only sites. The two read-modify-write-back sites (`saveTournamentOnly`, import) kept their plain parses, now covered by the 4.2 save wrap below — a corrupt value there throws and aborts loudly rather than a `[]`-returning reader overwriting the other saved tournaments. `getPlayerList()` is guarded the same way.
- **Player-list import can't brick Registration.** Imported names are filtered to non-empty strings before use; a single number or object in the file previously threw at `name.toLowerCase()` on every subsequent render, breaking the page until localStorage was hand-edited.
- **Server ID generation works over plain HTTP.** `crypto.randomUUID()` is undefined in non-secure contexts (self-hosted `http://<lan-ip>` is a supported deployment); it now falls back to `crypto.getRandomValues()`, so QR payload identity survives HTTP deployments instead of crashing config load.
- **Analytics register tabs show an error instead of a blank panel.** `renderTournamentList()` and `renderAllMatches()` now mirror the dashboard's try/catch-with-placeholder on a DB read failure.
- **Analytics can't paint stale data on rapid toggles.** Each of the three point-mode-sensitive views (dashboard, leaderboard, players) now carries a per-view generation token: a render captures it at entry and bails before painting if a newer render of the same view has started. Rapidly toggling point mode / layers / scope can no longer let a slower, older render win the repaint.
- **Malformed tournament imports fail cleanly instead of crashing at render.** `validateTournamentData` now rejects the crash-inducing shapes — a non-string player name (crashes `name.toLowerCase()`), a match without a string `id` (crashes `id.startsWith('BS-')`) or with a non-object player slot, or non-numeric `placements` — and coerces `completed` to boolean. Scope deliberately stops at crash-prevention: deeper *semantic* validation (are the match ids real bracket slots?) and an export tamper-CRC were considered and declined, because the data is browser-local with no system of record and no auth/secrets — a tampered import only ever affects the importer's own copy (rationale in the plan's Appendix A.3).
- **Running out of storage no longer loses a match silently.** localStorage has a ~10 MB cap; a long-lived instance that never prunes can fill it, and a `QuotaExceededError` mid-save previously threw into the match-completion flow — the completed match lost on reload, no message. Two contained pieces: a **pre-emptive gate** blocks creating or importing a tournament when storage is ≥90% full, with a clear "free up space" message (and the reassurance that deleting a finalized tournament keeps its stats in Analytics); and a **loud-fail wrap** around `saveTournamentOnly()` that, on a quota error *or* a corrupt registry, alerts once and aborts without a partial/overwriting write instead of throwing. The write order (list first) means the most likely failure leaves both keys untouched and consistent. Deliberately *not* done: the original write-reordering / caller-contract surgery — the gate makes a mid-save quota failure rare enough that the 5-line wrap is a sufficient backstop.

### Interface

- **Storage Space dialog restyled to the new landscape dialog system.** Migrated from the old `.modal-content` layout to the `.dlg dlg--wide` component — metrics in a left sidebar, usage bar + guidance in the main column — matching the Reset/Delete dialogs. Reused the existing dialog classes (no new CSS), and removed ~60 lines of orphaned `storage-*` chrome CSS left behind by the migration.

### Files changed

- `js/clean-match-progression.js` — `generateTransactionId()` helper + all mint sites; `advancePlayer()` failure return; `snapshotPlayerStats()` guard; Bo1 loser max; winner-confirmation listener fix; dead undo cluster / singular auto-advance / duplicate export block removed
- `js/bracket-rendering.js` — QR-aware undo gates (4 sites); NewtonDB cleanup of rolled-back matches; undo modal fallback; redo system / `rebuildBracketFromHistory` / unreachable fallbacks / dead else removed; JSDoc corrected; Phase 3 name escaping (match cards, command center, referee dropdowns/suggestions, paid-fee lists)
- `js/tournament-management.js` — strict-matchId export pruning; `playerList` restore key; `isOldFormatImport` on overwrite path; watermark walkover filter; debug-log-before-null-check; `showingAllTournaments`/`toggleTournamentView` removed; new `readTournamentsRegistry()` guarded reader (6 read-only sites); Phase 3: escaped tournament status / Recent Tournaments / import messages / watermark, `loadRecentTournaments` delegated listener (server filename + ids out of onclick); Phase 4.4: deeper `validateTournamentData` crash-prevention (string player names, match shape, numeric placements); Phase 4.2: `storageGateBlocks()` gate at create/import + loud-fail try/catch on `saveTournamentOnly`; Storage Space dialog rebuilt as `.dlg--wide`
- `css/styles.css` — removed ~60 lines of orphaned `storage-*` dialog chrome CSS (after the Storage Space restyle)
- `js/analytics.js` — strict-matchId pruning (3 sites) + id-less transaction handling; Phase 3: removed the private non-quote-safe `escapeHtml` (now uses the canonical one), escaped Developer Console views + `showCommandFeedback`
- `js/newton-table.js` — Phase 3: row clicks dispatch by integer index into the data array (removed `_rowId`-in-onclick and the flawed `_escAttr`) — hardens every table at once
- `js/newton-history.js` — match detail tournament record; import confirm handler cleanup; tiebreak legs excluded from averages; register-tab render error placeholders (`renderTournamentList`/`renderAllMatches`); Phase 3: cell action buttons moved to `data-*` + a shared capture-phase delegated listener (`_wireTableActions`), cell escaping; Phase 4.8: per-view render-race tokens in `renderDashboard`/`renderLeaderboard`/`renderPlayersTab`
- `js/results-config.js` — stale `generateResultsCSV` export removed; dead `saveConfiguration()` removed; `crypto.randomUUID` HTTP fallback for server-ID generation; Phase 3: results-table player-name escaping
- `js/lane-management.js` — `requireLaneForStart` pre-start enforcement; dead bootstrap + `updateLaneConfiguration()` removed; Phase 3: referee `<option>` name escaping
- `js/player-management.js` — `shortLegs: []` init (2 sites); dead `safeSaveTournament()` removed; `getPlayerList()` corrupt-data guard; player-list import string filter; Phase 3: `renderPlayerList` per-render index + delegated listener (the O'Brien fix), name escaping
- `js/qr-bridge.js` — result payload leg validation before scanner teardown; Phase 3: escaped the QR result preview and QR Payload Inspector payload fields, including the raw-string JSON-parse-failure branch
- `js/main.js` — sequential logo loading; Phase 3: canonical quote-safe `escapeHtml()` (window-exported); match-history delegated listener + heading/name escaping
- `js/bracket-lines.js` — invisible placeholders, zero-sized divs, dead `bronzeX` branches, dead `finalsX` parameter removed; Phase 3: escaped the bracket headers (club + tournament name)
- `js/types.js` — stale `rebuildBracketFromHistory` doc line removed
- `chalker/js/chalker.js` — cross-leg undo recalculation; tiebreak/edit-checkout history routing; edit-mode delete; tiebreak cancel/undo; rematch from `state.config`; Phase 3: own `escapeHtml()` + escaped the QR-confirm modal, leg-breakdown headers, leg status, and match history
- `chalker/sw.js` — `ignoreSearch`, scanner libs cached, `addAll` error logging, `chalker-v109`
- `chalker/index.html` — `chalker.js?v=12`
- `api/upload-tournament.php` — `wasOverwritten` captured before write; 10 MB payload cap; tournament shape check
- `api/api-check.php` — normalized `NEWTON_API_ENABLED` kill switch
- `api/relay.php` — opt-in `NEWTON_RELAY_ALLOWLIST` host restriction
- `api/README.md`, `DOCKER-QUICKSTART.md`, `docker/docker-compose.yml`, `docker/docker-compose-ssl.yml` — new env var documented
- `Docs/CODE-IMPROVEMENT-PLAN.md` — new: full review findings and the six-phase plan (Phases 1, 2, 3, 5 and most of Phase 4 shipped — 4.1–4.2, 4.4, 4.5, 4.7–4.9; 4.3/4.6/4.10 deferred; Phase 6 → v5.1.6)

### Migration

No data migration required. The Chalker service-worker cache bump (`chalker-v109`) refreshes installed PWAs automatically on next online launch. Legacy `savedPlayers` localStorage keys are simply no longer written; existing ones are inert.

---

## **v5.1.4** — Pocket Chalker (2026-06-01)

### Chalker — PWA install banner

- **Install prompt** — a banner slides up from the bottom when the browser offers PWA installation: "Install NewTon Chalker for offline use." One tap triggers the native install dialog. Dismiss (✕) hides it permanently. Never shown again after install or dismissal (`chalker_install_dismissed` in localStorage). Never shown inside a TWA (Play Store install).
- **Styling** — dark theme (`--bg-dark`), blue accent border (`--accent-primary`), Manrope font. Respects safe area insets for notch/home bar.
- **CSP-clean** — all logic in `chalker.js`, all styling in `chalker.css`. No inline scripts or styles.

### Files changed

- `chalker/index.html` — install banner markup
- `chalker/js/chalker.js` — `beforeinstallprompt` capture, `installApp()`, `dismissInstall()`, `appinstalled` listener
- `chalker/styles/chalker.css` — `.install-banner`, `.install-btn`, `.install-dismiss` styles

---

## **v5.1.3** — Cannot Operate on Soup (2026-05-29)

Small polish on top of v5.1.2: trimmed a redundant helper line from the Confirm Winner dialog, restructured the Late Arrival info dialog so the actual instructions get visual focus, introduced a reusable `.dlg__note` panel component to the dialog system, and fixed a JSON-LD signal on the landing page that was letting Google (and AI services) treat the GitHub repo as the canonical home of the project instead of `newtondarts.com`.

### Confirm Winner — redundant helper removed

- **Dropped the italic "Winner must have more legs than loser" small-text** from the Final Score row. The same message is already raised dynamically by the form validator when the leg counts are inverted, so the static helper was pure redundancy. Cleaner row layout, one fewer line for the eye to parse.

### Late Arrival info dialog — focus on the actual instructions

- **First paragraph now adapts to the Developer Console setting.** When Developer Console is already enabled in Global Settings, the lead-in collapses to "To register a late arrival, open the **Developer Console** by clicking the version number in the lower-right corner of the Tournament Bracket." When Dev Console is disabled, the original two-step instruction (enable, then open) is shown. Operators who've already opted in no longer have to skip past instructions for a setting they've already configured. `showLateRegInfoModal()` reads `ui.developerMode` from `dartsConfig` (same pattern used elsewhere in `tournament-management.js`) and swaps the paragraph text before pushing the dialog.
- **The two action paragraphs ("use the Late Arrival command" + "the new player will be placed...") are now wrapped in a `.dlg__note` framed panel.** Single shared frame, white background, soft elevation shadow, 1px border, 8px radius — the actual operator steps get visual focus rather than dissolving into a wall of muted prose.
- **Text inside `.dlg__note` is darker than the default `.dlg__desc`** — overridden to the dialog body color `#2a2f3a` (from the muted `#6b7280` helper gray). The frame draws attention to the content inside it; the text needs to read as primary, not as helper prose.
- **New italic caption below the framed panel** — *"The Developer Console is for changes made after a tournament has started — outside the normal workflow."* Explains *why* this operation lives in the Developer Console rather than the main UI, in one line. Stays muted (default `.dlg__desc` gray) so it reads as an aside rather than competing with the framed instructions.

### New dialog component — `.dlg__note`

A small reusable building block added to the dialog system. White background (same as the dialog itself), 1px `#ecedf1` border, 8px radius, 14px/16px padding, and a soft elevation shadow (`0 1px 2px rgba(20,25,35,0.04), 0 4px 12px rgba(20,25,35,0.06)`). Stacked children get automatic 10px vertical spacing via `.dlg__note > * + *`. Scoped color override `.dlg__note .dlg__desc { color: #2a2f3a; }` so framed content reads as primary text. Currently used only by `lateRegInfoModal`; available to any future dialog that needs to group a procedure or callout inside its body.

### SEO / discoverability — JSON-LD canonical signal

- **Added explicit `url` and `codeRepository` to the `SoftwareApplication` JSON-LD** on both `landing.html` (static) and `landing-page.php` (the Docker-served variant). `url` now points at `https://newtondarts.com/` (the canonical site) and `codeRepository` points at `https://github.com/skrodahl/NewTon` (the source). Schema.org's `SoftwareApplication` inherits `codeRepository` from `CreativeWork`, so the GitHub link has a proper home that doesn't compete with the canonical URL signal. The PHP variant additionally fixed a latent bug: `"url"` was previously pointing at `$githubUrl` (the GitHub repo) — actively telling Google the canonical home was on github.com. That has been corrected to `$baseUrl` (with the same conditional-emission pattern used by the existing `og:url` and `<link rel="canonical">` fragments, so the field is omitted entirely when `NEWTON_BASE_URL` is unset rather than emitting an empty string).
- **Why it matters.** The schema was previously silent on `url`. With no explicit signal in the structured data, Google (and AI services that consume the same signals) fell back to link structure, backlinks, and domain authority — all of which favor GitHub by a wide margin. Result: search results and AI summaries kept pointing at `github.com/skrodahl/NewTon` as the project's home rather than at `newtondarts.com`. The HTML-level `<link rel="canonical">` was already correct; the JSON-LD now reinforces it instead of being silent.
- **The existing schema was already clean in other respects** — `offers` block correctly wraps price/currency, `license` is already an SPDX URL (`https://opensource.org/licenses/BSD-3-Clause`). The two new properties were the only meaningful addition needed. Validate with [Rich Results Test](https://search.google.com/test/rich-results) and [Schema Markup Validator](https://validator.schema.org/) after deploy.
- **Realistic expectations.** The structured-data fix tells Google what the canonical URL *should* be; it doesn't override GitHub's domain authority or backlink profile overnight. Correction will be gradual as Google re-indexes and as more references to `newtondarts.com` accumulate.

### Footer easter egg

A small `42` now trails the cheeky "No popups? No cookies!" line in the landing-page footer. Muted brown (`#c9b8a4`), monospaced, no underline, 70% opacity — sits at the edge of attention. On hover it firms up to the standard footer-link brown. Clickable; where it goes is left as an exercise for the reader, with bonus points for anyone who has *The Long Dark Tea-Time of the Soul* within arm's reach. New `.footer-egg` CSS class added next to `.footer-cheeky` in `css/landing.css`; identical markup added to both `landing.html` and `landing-page.php`.

### Documentation

- **`Docs/DIALOGS.md`** — new "The `.dlg__note` panel" section between Buttons and Helpers, with anatomy, when-to-use guidance, and the text-color rationale. Migrated-dialogs table entry for Late Registration Info now reads `default + .dlg__note` so the use of the new component is discoverable from the index.

### Files changed

- `tournament.html` — removed the "Winner must have more legs than loser" `<small>` from the Confirm Winner Final Score row; added `id="lateRegInfoFirstPara"` to the late-arrival lead paragraph; wrapped the two action paragraphs in `<div class="dlg__note">`; added italic caption paragraph below the framed panel
- `js/player-management.js` — `showLateRegInfoModal()` reads `ui.developerMode` from `dartsConfig` and swaps the first paragraph's `innerHTML` between the two predefined static strings before pushing the dialog
- `css/styles.css` — new `.dlg__note`, `.dlg__note > * + *`, and `.dlg__note .dlg__desc` rules added next to `.dlg__desc`
- `landing.html` — `SoftwareApplication` JSON-LD gains `url` (`https://newtondarts.com/`) and `codeRepository` (`https://github.com/skrodahl/NewTon`) properties; footer gains the `42` easter-egg link
- `landing-page.php` — JSON-LD `"url"` corrected from `$githubUrl` to a conditional `$jsonLdUrl` (uses `$baseUrl` when set); new `"codeRepository"` field added; footer gains the `42` easter-egg link
- `css/landing.css` — new `.footer-egg` and `.footer-egg:hover` rules added next to `.footer-cheeky`
- `Docs/DIALOGS.md` — new `.dlg__note` panel section; migrated-dialogs table entry updated for `lateRegInfoModal`

### Migration

No data migration required. CSS-only additions and a single conditional in `showLateRegInfoModal()`; all other dialogs are unaffected.

---

## **v5.1.2** — The Polished Press (2026-05-26)

Tactile press feedback on every button across the app, a new dialog system (`.dlg`) that unifies eleven confirmation/warning/info modals around a small set of shared layouts, a redesigned Tournament Complete celebration with an Olympic tiered podium and decorative garlands, a fix to a long-standing match-achievement attribution bug, and a small Registration-page bug fix.

### Button press feedback

- **Press effect across all buttons** — `transform: translate(1.5px, 1.5px)` on `:active` for `.btn`, `.nav-btn`, `.zoom-btn`, `.help-btn`, `.cc-match-action-btn`, `.achievement-export-btn`, `.tournament-create-button`, `.analytics-reset-btn`, `.analytics-back-btn`, `.close`. Every click now feels tactile rather than flat.
- **Analytics selector buttons excluded by design** — `.analytics-point-btn` and `.analytics-layer-btn` carry their own "active" indicator and don't inherit `.btn`, so they naturally opt out.
- **Removed `!important` from `.help-btn:hover`** — was blocking the unified `:active` rule. Hover now behaves consistently without overrides.
- **Removed obsolete `.celebration-export-btn:active` rule** — `translateY(0)` reset is no longer meaningful with the new shared press effect.

### New dialog system (`.dlg`)

A second modal pattern that sits inside the existing `.modal` backdrop and preserves `pushDialog`/`popDialog` infrastructure. Buttons inside dialogs use the standard `.btn` classes — no separate dialog button styles.

- **Base components:** `.dlg`, `.dlg__title`, `.dlg__desc`, `.dlg__label`, `.dlg__input`, `.dlg__foot` (white card, 10px corner radius, soft shadow, right-aligned footer with subtle top border).
- **Title separator** — `.dlg__title` carries `border-bottom: 1px solid #ecedf1` + `padding-bottom: 14px`, mirroring the footer's `border-top`. Every dialog gets a consistent header / body / footer rhythm automatically; no markup change needed in individual dialogs.
- **Split layout:** `.dlg--split` modifier with `.dlg__grid`, `.dlg__sidebar`, `.dlg__sidebar-label`, `.dlg__sidebar-value`, `.dlg__main` — metadata sidebar on the left, action area on the right.
- **Wide modifier:** `.dlg--wide` adds ~20% width (880px from 720px). For dialogs with three-button footers or richer body content. Stackable with `.dlg--split`.
- **Sidebar widened** — `.dlg__grid` left column 220px → 260px (+18%). System-wide. Improves readability of longer values ("Frontside Round 1") and accommodates sidebar buttons (winner-confirm Edit Statistics).
- **Intent pills:** `.dlg__pill` (amber) and `.dlg__pill--danger` (red) — small uppercase badges at the top of the main area. Pills can be omitted entirely (additive actions, info dialogs) or toggled conditionally (e.g. shown only for legacy-format imports).

### Dialog migrations

Eleven dialogs migrated. Most use `.dlg--split` with the standard tournament-metadata sidebar; some use match metadata instead (when the dialog is about a specific match rather than the tournament); one uses the default single-column layout (pure info); one uses `.dlg--split + .dlg--wide` (winner-confirm needs the extra room for its form and three-button footer).

- **Reset Tournament Confirmation** — amber "Destructive" pill, sidebar shows Name / Tournament Status / Progress / Players. Confirmation input retained with the tournament name shown as placeholder. Markup down from ~47 lines of inline-styled boxes to ~22 lines.
- **Delete Tournament Confirmation** — red "Destructive" pill, sidebar shows Name / Date / Tournament Status / Players. A second paragraph explicitly notes that the tournament's Analytics record is preserved and remains available for review.
- **Import Tournament Overwrite** — amber "Destructive" pill, sidebar shows Name / Date / Tournament Status. "Alternative: rename the imported file to keep both" kept as a secondary paragraph.
- **Tournament In Progress Warning** — amber "Warning" pill, sidebar shows live tournament state (Name / Tournament Status / Progress / Players). Body trimmed from 3 colored tip boxes to 2 paragraphs.
- **Import Tournament Confirmation** — amber "Old format" pill, conditionally shown only for legacy exports. Sidebar shows Name / Date / Tournament Status / Progress / Players. Description condenses the old "Data Safety Guaranteed" box into one sentence; the verbose "What will be imported" list is gone (redundant with file context). Button text toggles "Import Tournament" / "Import Anyway" based on format.
- **Load Tournament Confirmation** — no pill (load is non-destructive). Sidebar shows the tournament being loaded (Name / Date / Tournament Status / Progress / Players). Description varies — when an active tournament exists it names it inline ("Your current tournament *X* will be saved automatically and replaced..."), otherwise reads as a clean "The selected tournament will become active." Replaces the old dual-section layout that duplicated metadata.
- **Undo Match Confirmation** — split layout with *match metadata* sidebar (Match / Bracket / Players) instead of tournament metadata. Amber "Destructive" pill. Body refactored to build DOM via `createElement` / `textContent` rather than `innerHTML` interpolation — **closed a pre-existing XSS hole** where player names were being inserted into HTML via template literals. New helpers `_buildUndoMatchCard()`, `_buildUndoAchievementsSection()`, and `_bracketLabel()` (shared with winner-confirm). The old `createUndoModalContent()` HTML-string builder (~80 lines) deleted entirely.
- **Analytics — Delete Tournament** (`historyDeleteTournamentModal`) — split layout, red "Destructive" pill, sidebar shows Name / Date / Tournament Status (hardcoded "Completed" since Analytics history only contains finalized tournaments). Confirmation input retained with the tournament name in the placeholder.
- **Late Registration Info** — pure info, **uses the default (non-split) layout** — first dialog to do so. No pill, no sidebar. Title + three description paragraphs walking the operator through enabling the Developer Console → opening it → using the "Late Arrival" command. Single "Got it" button.
- **Export Tournament Results** — split layout, standard 5-field tournament sidebar. Conditional amber "Incomplete" pill + warning paragraph (toggled together) for tournaments without finalized rankings. Description condensed to a one-line "Save as {format}: {filename}" with format and filename bold-emphasized. Dropped the verbose "What will be included" list, the separate "Export Details" box, and the "Tournament Complete" reassurance box.
- **Confirm Match Winner** — most involved migration. Uses **split + `.dlg--wide`** (3-button footer + leg-score form needs the extra room). Sidebar carries match metadata (Match / Bracket) plus an "Edit Statistics" section with the two player names as full `.btn` buttons — clicking either opens the stats modal. Dynamic title reads *"{winner} beats {loser}"*. Body shows a Match Progression block (small uppercase header + colored progression lines: green winner, red loser, plain text without tick/bullet markers) and the existing leg-score form. Three-button footer: Cancel / Scan Results QR / Confirm Winner. Body content refactored from `innerHTML` template-literal interpolation (player names XSS hole) to `createElement` / `textContent` throughout. New `_buildWinnerProgressionBlock()` helper. All form validation, snapshot/restore, button wiring, and Enter-key handling preserved unchanged.

### Unified Tournament Status across all sidebars

- **New helper `tournamentStatusLabel(t)`** in `js/tournament-management.js` — returns `Setup` / `Active` / `Completed` for any tournament-shaped object (current, saved in localStorage, or imported file). Prefers the explicit `status` field; falls back to deriving from `bracket + matches` state for older tournaments and old-format imports.
- **Tournament Status now displayed in every dialog's sidebar** that uses `.dlg--split` and is about a tournament (not a specific match). Delete's previous "Setup / Bracket Generated / In Progress" scheme was unified to the same labels as everything else for consistency.

### Tournament Complete celebration — Olympic tiered podium

The Tournament Complete view inside Match Controls now uses an Olympic-style tiered podium:

- **Three tiered colored blocks** under three white cards: gold (160px, center, tallest), silver (120px, left), bronze (88px, right). Block surfaces use metallic gradients matching the medals.
- **Drop shadow on each block** gives them physical depth.
- **20px gap** between podium columns lets each column read as its own piece while the composition still reads as one object.
- **CSS-only metallic medal discs** sit on each card — gold/silver/bronze radial gradients with the rank number inside. Replaces the previous emoji medals (🥇🥈🥉).
- **Uppercase rank labels** — CHAMPION / SILVER / BRONZE — match the sidebar-label style used elsewhere in the dialog system.
- **Decorative SVG garlands** — four wavy strands (muted red / cream / green / black) sweep across the background behind the podium, giving the celebration view a sense of occasion without resorting to emoji.
- **Tournament Highlights row** sits cleanly below the podium — three side-by-side cards on a single white background. No heading (the row's contents speak for themselves).
- **All decorative emojis removed** — title goes from "🏆 X Complete! 🏆" to "X Complete!", the highlights heading is gone entirely.
- **Buttons untouched** — the Tournament Analytics button keeps `.btn` styling and the unified press effect.

### Documentation

- **New `Docs/DIALOGS.md`** — comprehensive reference for the `.dlg` system. When to use which pattern, anatomy, visual structure rules (the title + footer separator pair), the two layouts plus the `.dlg--wide` modifier, sidebar conventions (including the tournament-metadata-vs-match-metadata distinction), the intent pill system with all variants, button rules, helpers, XSS safety, dialog stack integration. Includes the live migration status table and an 8-step migration checklist for the next dialog. Updated through the session as each new dialog was migrated.

### Bug fix — match achievement attribution

A latent bug in the achievement snapshot/diff: the baseline was captured when the Confirm Winner modal opened, which was a loose moment in the lifecycle. If the dialog was opened previously for the same match (and exited via an abnormal path) or if other matches were completed between opens, the snapshot could capture player stats at the wrong instant. The diff at confirm-time would then record achievements from earlier matches onto the current match's transaction — and undoing the current match would incorrectly revert those earlier achievements too.

The fix anchors the snapshot to the **Start Match** event instead:

- New field: `match.preMatchSnapshot = { [player1Id]: snap, [player2Id]: snap }`
- Recorded in `toggleActive()` when the match becomes active. Overwritten on every Start (Esc/Cancel having already rolled back any pending stats from a previous winner-confirm session).
- `showWinnerConfirmation` reads the anchored snapshot via the existing `_completionSnapshot` global — now a pointer into `match.preMatchSnapshot` rather than a session-scoped copy.
- `handleConfirm` and `handleCancel` no longer null the pointer — the snapshot lives on the match object for the next attempt (and stays after completion as audit data).
- Fallback: if a match was started before this lands and has no snapshot, `showWinnerConfirmation` takes one at dialog open — same behavior as the previous approach, so existing in-progress tournaments keep working without migration.

Result: every `COMPLETE_MATCH` transaction's `achievements` field correctly reflects only the achievements added between **this match's** Start and Confirm — no leakage between matches, no contamination across dialog re-opens. Undo now reverts exactly the right set per match.

A follow-up will close the remaining gap: stats added via the bare `openStatsModal` (Results table, Command Center) outside any winner-confirm flow are still untracked per-match. The plan is to attribute them to the player's last completed match transaction. Deferred — see `Docs/UNDO.md` for the rationale.

### Bug fix — late-reg button visibility

- **"Player arrived late?" button now appears immediately when navigating to Registration mid-session**, not only after a reload. `showPage('registration')` was calling `renderPlayerList()` (the saved-players list) and `updateRegistrationPageLayout()` but not `updatePlayersDisplay()` — the function that renders the tournament-players list AND the late-reg button container. Now called as part of the registration page activation.

### Cleanup

- Removed `#resetConfirmationInput` from the `border-radius: 0` cascade selector — the new `.dlg__input` uses rounded corners by design.
- Removed orphaned banner CSS (`.dlg__banner`, `.dlg__banner-dot`, `.dlg__banner-title`, `.dlg__banner--danger`) — used briefly during the Tournament-in-Progress migration before that dialog was converted to split layout for consistency.
- Removed orphaned `.celebration-export-btn` and `.celebration-actions` styles — both classes had CSS defined but were not referenced anywhere in HTML or JS. Also dropped `.celebration-export-btn:active` from the press-effect selector list.
- Renamed `_undoBracketLabel` → `_bracketLabel` — now shared between undo and winner-confirm dialogs.
- Deleted `createUndoModalContent()` (~80 lines of HTML-string building) — replaced by safe DOM construction.
- Removed orphaned `.clickable-player-name` CSS — used briefly during the winner-dialog migration before the sidebar names became proper `.btn` buttons.
- Removed orphaned `.winner-hint` CSS — used briefly during the winner-dialog migration for the "💡 Click player names" hint, before the hint was removed entirely (the sidebar label "Edit Statistics" is self-explanatory).

### Files changed

- `css/styles.css` — unified `:active` press-effect rule; removed `!important` from `.help-btn:hover`; new `.dlg` system (base + `--split` + `--wide` + pills, title separator, 260px sidebar); removed `#resetConfirmationInput` from border-radius cascade; removed banner classes; rewrote tournament-celebration / podium / highlights styles for the Olympic tiered podium with garlands; new `.winner-progression*` and `.winner-stats-stack` classes; removed orphaned `.celebration-export-btn`, `.celebration-actions`, `.clickable-player-name`, `.winner-hint`
- `tournament.html` — replaced eleven modal markups with `.dlg` structure (Reset, Delete, Import Overwrite, Tournament In Progress, Import Confirmation, Load Tournament, Undo Match, Analytics Delete, Late Reg Info, Export, Winner Confirm); rewrote `#tournamentCelebration` for the Olympic podium with inline SVG garlands
- `js/tournament-management.js` — new `tournamentStatusLabel()` helper; `showResetTournamentModal()`, `showDeleteTournamentModal()`, `showImportOverwriteModal()`, `showImportConfirmModal()`, `showLoadTournamentModal()` updated for new sidebar values and all use the helper; conditional pill toggle in import-confirm
- `js/clean-match-progression.js` — `showTournamentProgressWarning()` populates new sidebar fields via the helper; `showWinnerConfirmation()` refactored for safe DOM building (sidebar populated via `textContent`, body built via `createElement`, dynamic title, new `_buildWinnerProgressionBlock()` helper, Edit Statistics buttons wired in sidebar); achievement snapshot anchored to `toggleActive()` (Start Match) via new `match.preMatchSnapshot`; `showWinnerConfirmation`/`handleConfirm`/`handleCancel` read from the anchored snapshot instead of taking a fresh one at dialog open; `_completionSnapshot` global is now a pointer into the match's snapshot rather than a session-scoped copy
- `js/bracket-rendering.js` — `updateCelebrationSubtitle()` drops emojis from the dynamically-set title; `showUndoConfirmationModal()` refactored for safe DOM (new `_buildUndoMatchCard()`, `_buildUndoAchievementsSection()`, `_bracketLabel()` helpers); `handleSurgicalUndo()` updated to pass structured params; old `createUndoModalContent()` deleted
- `js/newton-history.js` — `promptDeleteTournament()` updated for new sidebar IDs (separate name and date, placeholder pattern instead of dynamic prompt `innerHTML`)
- `js/results-config.js` — `showExportConfirmModal()` trimmed: new sidebar IDs, conditional pill + warning toggle, dropped the verbose "what will be included" list builder
- `js/main.js` — `showPage('registration')` now calls `updatePlayersDisplay()` so the late-reg button reflects current tournament state on every navigation
- `Docs/DIALOGS.md` — new
- `Docs/UNDO.md` — updated: replaced the "Planned — Match-Anchored Snapshot" section with implementation notes; reframed the "Completion Modal — Snapshot & Diff" section around the new Start-Match anchor; the manual-entry gap remains in "What This Does Not Solve" (deferred follow-up)

### Migration

No data migration required. Visual/interaction polish only — all functionality, dialogs, and data flows preserved. The remaining 11 modals continue to use the existing `.modal-content` pattern and can be migrated incrementally; `matchCommandCenterModal` (Match Controls) and the Developer Console are intentionally excluded from the `.dlg` rollout — they're content-dense custom layouts.

---

## **v5.1.1** — Somewhat Simpler Selfhosting (2026-05-20)

Docker release. No tournament-app changes. The container's mDNS code never worked reliably on a typical Linux host (avahi conflict) and has been removed entirely. `.local` resolution is now documented as a host-side concern.

### Docker — mDNS removed from container

- **Container avahi/dbus removed** — `avahi`, `avahi-tools`, and `dbus` packages dropped from the Dockerfile. The container no longer attempts to start its own avahi-daemon. mDNS broadcasting belongs to the host OS, not the container.
- **Reason:** with `network_mode: host`, the container's avahi conflicted with the host's avahi (most desktop Linux distros). With port mapping, the container couldn't broadcast at all. There was no configuration where the in-container avahi reliably worked.

### Docker — environment variables

- **`SSL_HOSTNAME` (new)** — replaces `MDNS_HOSTNAME`. Controls the Subject Alternative Name on the auto-generated SSL certificate. The old name still works as a fallback (`HOSTNAME="${SSL_HOSTNAME:-${MDNS_HOSTNAME:-newtondarts}}"`) so existing compose files keep functioning.
- **`HTTP_PORT` (new)** — configurable HTTP listening port (default `2020`). Mirrors the existing `HTTPS_PORT`. Useful for `network_mode: host` deployments avoiding privileged ports.

### Docker — compose cleanup

- **`docker-compose-ssl-mdns.yml` removed** — host-networking variant is no longer needed for mDNS (since mDNS isn't a container concern). Users on this file should switch to `docker-compose-ssl.yml` (port-mapped) or `docker-compose.yml` (minimal).
- **`docker-compose.yml` switched to port mapping** — `network_mode: host` removed from the default compose. Added port mapping `2020:2020` and commented SSL options.
- **`docker-compose-ssl.yml`** — added commented `HTTP_PORT`, `SSL_HOSTNAME` options and a pointer to `Docs/MDNS.md`.

### Docker — nginx templates parameterized

- **`nginx-http.conf`, `nginx-ssl.conf`** — `listen 2020;` replaced with `listen ${HTTP_PORT};`. Entrypoint expands both `${HTTP_PORT}` and `${HTTPS_PORT}` via `envsubst` for both HTTP and HTTPS modes.

### Documentation

- **`Docs/MDNS.md` (new)** — platform-by-platform mDNS setup. macOS (Bonjour built in), Windows (install Bonjour), Linux (avahi-daemon + `allow-interfaces` to exclude Docker bridges). Troubleshooting section covers avahi advertising Docker bridge IPs, HSTS port-change browser cache, and client-side `.local` compatibility (iOS/Android/Windows/Linux).
- **`DOCKER-QUICKSTART.md`** — "Mac / Windows" section renamed to "SSL (All Platforms)". "Linux — SSL + mDNS" section removed. New "LAN Access via `<hostname>.local`" subsection points to `Docs/MDNS.md`. Env var table updated: removed `MDNS_HOSTNAME` (deprecated note in `SSL_HOSTNAME` row), added `HTTP_PORT`. Troubleshooting `https://newtondarts.local Not Reachable` section rewritten.
- **`docker-quickstart.html`** — same edits as the markdown for parity.
- **`llms.txt`** — Docker SSL section updated. Listed `HTTP_PORT`/`HTTPS_PORT`/`SSL_HOSTNAME`, removed `MDNS_HOSTNAME` description, added pointer to `Docs/MDNS.md`. Compose file list updated to reflect removal of `docker-compose-ssl-mdns.yml`.

### Docker — entrypoint cleanup

- **`entrypoint.sh`** — avahi/dbus startup block (lines 8–42 in the previous version) removed entirely. New `HTTP_PORT` variable, renamed `HOSTNAME` derivation to use `SSL_HOSTNAME` with `MDNS_HOSTNAME` fallback. Comment in HTTP-mode branch references `${HTTP_PORT}` instead of hardcoded 2020.

### Third-party licenses

- **`THIRD-PARTY-LICENSES.md` was missing from the Docker image** — `.dockerignore` excluded `*.md`, including the license file. A compliance gap for the bundled MIT (qrcode-generator, QrScanner) and Apache 2.0 (jsQR) libraries.
- **Fix:** new `third-party-licenses.html` ships in the container (HTML isn't `.dockerignore`d), linked from the landing-page footer (`landing.html` and `landing-page.php`) alongside Privacy.
- The markdown stays in the repo as the canonical source.

### Release process

- **`releases/README.md`** — Step 6 expanded to cover both the chalker.js cache buster (`?v=N` query string) *and* the service worker `CACHE_NAME` in `chalker/sw.js`. Both must bump together — without the SW bump, installed PWAs keep serving stale code from offline storage.

### Migration

No migration required for the tournament app. Existing Docker deployments using `MDNS_HOSTNAME` continue to work via the entrypoint fallback. Users on `docker-compose-ssl-mdns.yml` should switch to `docker-compose-ssl.yml` or `docker-compose.yml`; `network_mode: host` is no longer required for `.local` access (the host's avahi/Bonjour handles the broadcast).

### Files changed

- `docker/entrypoint.sh` — removed avahi/dbus startup; added `HTTP_PORT`; `SSL_HOSTNAME` with `MDNS_HOSTNAME` fallback; envsubst expanded to handle both modes
- `docker/Dockerfile` — removed `avahi`, `avahi-tools`, `dbus` packages
- `docker/nginx-ssl.conf` — `listen ${HTTP_PORT};` for the redirect listener
- `docker/nginx-http.conf` — `listen ${HTTP_PORT};` for the main listener
- `docker/docker-compose.yml` — switched from `network_mode: host` to port mapping; removed `MDNS_HOSTNAME` ref; added commented SSL/port options
- `docker/docker-compose-ssl.yml` — added commented `HTTP_PORT`/`SSL_HOSTNAME` options; pointer to `Docs/MDNS.md`
- `docker/docker-compose-ssl-mdns.yml` — **deleted**
- `Docs/MDNS.md` — new
- `DOCKER-QUICKSTART.md` — Linux section removed; LAN access pointer added; env var table updated; troubleshooting rewritten
- `docker-quickstart.html` — parallel edits to the markdown
- `llms.txt` — Docker SSL section updated
- `third-party-licenses.html` — new
- `landing.html` — footer link `THIRD-PARTY-LICENSES.md` → `third-party-licenses.html`
- `landing-page.php` — same footer link update
- `releases/README.md` — Step 6 expanded to cover service worker `CACHE_NAME` bump
- `js/main.js` — `APP_VERSION` → `5.1.1`
- `chalker/js/chalker.js` — `CHALKER_VERSION` → `5.1.1`
- `chalker/index.html` — chalker.js cache buster `?v=8` → `?v=9`
- `chalker/sw.js` — `CACHE_NAME` → `chalker-v106`
- `releases/index.html`, `releases/v5.1.1.html` — release notes
- `sitemap.xml` — `v5.1.1.html` entry

---

## **v5.1.0** — Burgerboy85-rgb to Throw (2026-04-23)

### Analytics — Players Tab

- **Player list** — checkbox + player name. All selected by default. Header checkbox toggles all. Scrollable (70vh desktop, 40vh mobile). No pagination.
- **Split layout** — player list on the left (300px), profile/comparison panel on the right. Stacked on mobile (<1120px).
- **Selection-driven profile panel**:
  - All/multiple selected → comparison table (Player, Played, W/L)
  - One selected → individual profile card
  - None selected → placeholder
- **Cross-view navigation** — Leaderboard row click and Dashboard card click navigate to Players tab with that player focused. Works even if Players tab hasn't rendered yet (pending focus mechanism).
- **Dashboard drill-through** — 180s → Players tab, Highest Checkout → player profile, Shortest Leg → player profile.
- **Player selection persisted** — selection saved to localStorage (`newton_analytics_playerSelection`), restored on reload with stale name filtering.
- **Row click toggles checkbox** — click anywhere on a player list row to toggle selection. Hover effect on all tables.
- **Comparison table via NewtonTable** — sortable, consistent hover and styling.

### Analytics — Leaderboard Export

- **CSV and JSON export** — Export CSV and Export JSON buttons on the Leaderboard. Respects scope, point mode, and layers — what you see is what you export.
- **Shared CSV utility** (`newton-csv.js`) — `NewtonCSV.exportCSV()` and `NewtonCSV.downloadFile()`. Used by both leaderboard export and tournament results export. Refactored `results-config.js` to use the shared utility.

### Documentation

- **Documentation index** — all doc pages listed at the bottom of the User Guide with descriptions.

### Analytics — cache invalidation on finalization

- **New tournament appears without reload** — `NewtonHistory.invalidateCache()` called after tournament finalization completes (after placements and achievement reconciliation). The Analytics tab picks up the new tournament on next navigation without requiring a page reload.

### Navigation — remember active page

- **Page persists across reloads** — the active page is saved to `newton_activePage` in localStorage on every navigation. On reload, the app restores the last visited page instead of always returning to Tournament Setup.

### Global Settings — documentation links

- **Server Settings** header and **Automatically backup tournaments on completion** checkbox now have ℹ️ icons linking to `docker-quickstart.html` and `automatic-backup.html` respectively. Opens in a new tab. Links point to local files — works on both Docker and file-based deployments.

### Referee Suggestions — lane info

- **Lane shown in all suggestion categories** — Recent Losers, Recent Winners, and Recent Assignments now display the lane number when assigned (e.g. `Nick (FS-R1 · Lane 3)`). Helps operators identify who is already at the right board — especially useful for "loser marks" workflows.
- Inspired by [@burgerboy85-rgb](https://github.com/burgerboy85-rgb) — [Discussion #6](https://github.com/skrodahl/NewTon/discussions/6)

### Known issues

- Comparison table does not go full width below 1022px

### Files changed

- `tournament.html` — Players tab split layout, Leaderboard export buttons, `newton-csv.js` script tag
- `js/newton-history.js` — `renderPlayersTab()`, `renderProfilePanel()`, `focusPlayer()`, `togglePlayer()`, `toggleAllPlayers()`, `exportLeaderboardCSV()`, `exportLeaderboardJSON()`, player selection persistence, dirty-flag wiring, dashboard card targets
- `js/newton-csv.js` — new shared CSV/download utility
- `js/results-config.js` — refactored to use `NewtonCSV` for CSV export and file download
- `js/main.js` — save/restore active page via `newton_activePage` in localStorage
- `js/clean-match-progression.js` — `NewtonHistory.invalidateCache()` after finalization chain
- `js/bracket-rendering.js` — lane info added to Recent Losers, Recent Winners, and Recent Assignments in referee suggestions
- `css/styles.css` — `.players-split` flex layout, `.players-list-panel` scroll, `#playersTableContainer` hidden pagination, `.analytics-export-btn`, responsive stacking at 1120px
- `userguide.html` — Documentation index section, Automatic Backup links

---

## **v5.0.15-beta.2** — Offline at the Oche (2026-04-21)

### Documentation

- **Automatic Backup page** — new doc page (`automatic-backup.html`) covering local backup on tournament completion, remote backup to an Analytics instance, the venue-to-cloud workflow, Analytics Mode, and manual fallback. Includes Server Settings screenshot.
- **Cross-links** — userguide.html and docker-quickstart.html link to the new Automatic Backup page.

### Docker

- **Analytics Mode unlock** — undocumented `?tm=1` query parameter unlocks the full TM interface when in Analytics Mode. Enables retroactive achievement corrections.

### Files changed

- `automatic-backup.html` — new documentation page
- `Screenshots/backup.png` — Server Settings screenshot
- `tournament.html` — `?tm=1` query parameter overrides `NEWTON_MODE=analytics`; body class uses `$appMode` variable instead of raw `getenv()`
- `userguide.html` — links to Automatic Backup page
- `docker-quickstart.html` — link to Automatic Backup page
- `releases/README.md` — beta release process, cache buster step, version format note

---

## **v5.0.15-beta.1** — Ka-ZXing! (2026-04-20)

### Chalker — QR Decoder

- **QrScanner (ZXing) replaces jsQR as primary decoder** — the image capture path on iOS/iPadOS now uses QrScanner, a ZXing-based decoder that handles perspective distortion, glare, and screen reflections. jsQR consistently failed on iPad photos of QR codes on screens; QrScanner succeeds even at steep angles and poor lighting.
- **1500px downscale** — all captured photos are downscaled to max 1500px before decoding. Faster and more reliable than full-resolution processing.
- **BarcodeDetector removed from image capture path** — confirmed unavailable on iOS Safari (iPadOS 26). Removed dead code; BarcodeDetector remains in the TM's live camera scanner where it works.
- **"Not set" for missing lane/referee** — the QR confirmation modal now shows "Not set" instead of "undefined" when lane or referee are not assigned.

### Layout

- **Responsive breakpoint** — moved from 1024px to 1120px, covering tablets in portrait mode (e.g. iPad Air at 1080px).
- **Denser desktop tables** — `history-table` cell padding halved from `14px 12px` to `7px 6px`, header padding from `10px 12px` to `5px 6px`, badge cells from `14px 12px` to `7px 6px`. Shows 24 players on a desktop screen.
- **Sticky header fix** — removed `overflow-x: hidden` from `.page` below 1120px, which was creating a scroll container that broke `position: sticky` on table headers.
- **Scope indicator preserved** — the Analytics scope pill was hidden below 1120px because it sat inside `.history-actions` which was set to `display: none`. Fixed by hiding only the `.btn` children instead of the container.

### Files changed

- `chalker/js/chalker.js` — QrScanner as primary decoder in `decodeImageQR()`, 1500px downscale, BarcodeDetector removed from image path, "Not set" for missing lane/ref, version bump to 5.0.15-b.1
- `chalker/index.html` — added `qr-scanner.umd.min.js` script tag
- `chalker/js/qr-scanner.umd.min.js` — QrScanner/ZXing UMD build (~16KB)
- `chalker/js/qr-scanner-worker.min.js` — QrScanner Web Worker (~44KB)
- `chalker/qr-scanner-worker.min.js` — duplicate worker at page root (dynamic import path resolution)
- `Docs/QR.md` — updated decoder chain, library table, reliability notes
- `css/styles.css` — breakpoint 1024px → 1120px (all three media queries); desktop `history-table` td/th/badge padding halved; `.page` overflow-x removed from responsive block; `.history-actions .btn` hidden instead of `.history-actions`

---

## **v5.0.14** — Narrowing the Scope (2026-04-19)

### Navigation

- **Nav underline replaced** — the sliding `::after` pseudo-element (positioned by JavaScript) replaced with per-button `border-bottom`, matching the Analytics tab pattern. Fixes broken underline position when nav wraps to two lines. `updateNavUnderline()` function and resize listener removed.
- **Analytics button on bracket** — added to the bracket nav panel after Config. Navigates to the Analytics tab without leaving the bracket first.

### Mobile Layout (< 1120px)

- **Reduced padding** — container, page, config sections, and Analytics header all tightened for narrow screens.
- **Denser tables** — smaller cell padding, font size, and row spacing across all tables.
- **Compact player names** — 12px instead of 15px in table cells.
- **Header** — smaller title font (1.5rem), hidden clock, reduced padding/gap.
- **Analytics** — description and import/export buttons hidden.
- **Global Settings** — narrower number inputs, compact buttons with nowrap.

### Files changed

- `css/styles.css` — nav underline: removed `.nav::after` sliding indicator, added `border-bottom` on `.nav-btn` and `.nav-btn.active`; `@media (max-width: 1024px)` rules: container/page padding, header title/clock/padding, table density, player name font size override, Analytics description/buttons hidden, config section padding, config button sizing/nowrap
- `js/main.js` — removed `updateNavUnderline()` function, init call, and resize listener
- `tournament.html` — Analytics button added to `.bracket-controls-middle-left`

---

## **v5.0.14-beta.3** — I Can See Clearly Now (2026-04-17)

### QR Scanning

- **Larger QR on TM** — assignment QR cell size increased from 6px to 8px per module. Renders ~360-424px instead of ~270-318px — more pixels per module for the camera to capture.
- **Full-resolution decode on Chalker** — `decodeImageQR()` now tries full resolution first, then retries at 2000px if that fails. Beta.2's 1000px cap crushed QR module detail on iPhone 48MP photos.

### Docs

- **Release notes reorganised** — 86 files moved from `Docs/` to `Docs/ReleaseNotes/`.

### Contributors

- [@burgerboy85-rgb](https://github.com/burgerboy85-rgb) — iPhone 16 Pro Max + iPad Pro testing on iOS 26.4, identified the decode failure pattern

### Files changed

- `js/qr-bridge.js` — QR cell size 6 → 8 in `createSvgTag()`
- `chalker/js/chalker.js` — `decodeImageQR()` two-pass strategy: full resolution first, 2000px fallback
- `Docs/QR.md` — updated decode strategy documentation
- `Docs/ReleaseNotes/` — all release notes moved from `Docs/`
- `releases/README.md` — added pointer to `Docs/ReleaseNotes/`
- `CHANGELOG.md` — updated release notes path reference

---

## **v5.0.14-beta.2** — Picture This (2026-04-17)

### iOS QR Scanning — Fixes

- **Image loading fix** — replaced `URL.createObjectURL()` with `FileReader.readAsDataURL()` for iOS HEIF/HEIC compatibility. Beta.1 failed with "Could not load image" on iPhone 16 Pro Max.
- **User gesture fix** — replaced programmatic `fileInput.click()` with a styled "Take Photo" label button. iOS requires a direct user gesture to trigger file inputs; the programmatic approach failed silently, exposing the raw file input.

### Analytics

- **Date column nowrap** — tournament register date no longer wraps to two lines.
- **Match detail header** — redesigned: match ID and result as the headline (18px), tournament name (bold) · date · time on a single line below.

### Contributors

- [@burgerboy85-rgb](https://github.com/burgerboy85-rgb) — iOS testing on iPhone 16 Pro Max (iOS 26.4.1), identified both beta.1 failures

### Files changed

- `chalker/js/chalker.js` — `decodeImageQR()` switched to `FileReader.readAsDataURL()`; `startImageCapture()` uses label button; `stopQRScanner()` resets label visibility
- `chalker/index.html` — added `<label id="qr-image-label">` styled as "Take Photo" button
- `js/newton-history.js` — match detail header redesign; date column `nowrap`
- `Docs/QR.md` — updated implementation details and beta.1 lessons learned

---

## **v5.0.14-beta.1** — No Score... (2026-04-16)

*Beta — untested on iOS hardware. Feedback welcome via [GitHub discussion #4](https://github.com/skrodahl/NewTon/discussions/4).*

### iOS QR Scanning (Chalker)

- **Image capture for iOS/iPadOS** — on iOS devices, the Chalker uses native image capture (`capture="environment"`) instead of `getUserMedia`, which WebKit restricts. The user takes a photo of the assignment QR; jsQR decodes it client-side. Desktop/Android unchanged.
- **Platform detection** — `isIOS()` detects iPadOS 13+ (reports as "Macintosh") via `navigator.maxTouchPoints`.

### x01 Format

- **Expanded presets** — x01 format dropdown now offers 101 through 1001 in steps of 100 (previously 101, 201, 301, 501). Both TM Global Settings and Chalker manual setup.
- **Custom score** — "Custom score" checkbox reveals a number input (2–1001) for non-standard starting scores. TM shows an amber warning about QR payload size.

### Contributors

- [@burgerboy85-rgb](https://github.com/burgerboy85-rgb) — iOS QR workflow design, x01 format request

### Files changed

- `chalker/js/chalker.js` — `isIOS()`, `startImageCapture()`, `decodeImageQR()`, custom score toggle, updated `startQRScanner()` branching
- `chalker/index.html` — hidden file input, canvas, jsQR script tag, custom score checkbox
- `chalker/js/jsQR.js` — jsQR library copied from TM
- `js/results-config.js` — `initX01Toggle()`, custom score load/save/reset
- `tournament.html` — x01 presets expanded, custom score checkbox and warning
- `Docs/QR.md` — iOS image capture section updated from planned to implemented

---

## **v5.0.13** — The Pointy End (2026-04-16)

### Export / Import

- **Config in exports** — tournament JSON exports (v4.1) now include the full Global Settings (point values, match lengths). Previously this was lost, meaning imported tournaments inherited whatever settings happened to be active at import time.
- **Import respects config** — Analytics import uses the config from the JSON when available. Falls back to current Global Settings for older (pre-4.1) exports.
- **Import warning** — when importing a pre-4.1 export, the confirmation modal shows an amber warning explaining that current Global Settings will be used.

### Analytics

- **Reset button warm accent** — the Lens Reset button lights up with an amber border and warm tint when a filter is active, providing a clear visual cue that the view is scoped.
- **Leaderboard empty cells** — zero-value cells render as blank instead of em-dashes, reducing visual noise and letting actual numbers stand out. Points badge shows 0 explicitly.

### Files changed

- `js/tournament-management.js` — `config` field added to export payload, export version bumped to 4.1
- `js/newton-history.js` — import prefers `t.config` from JSON; import warning toggle; leaderboard empty cells; reset button `.has-filter` class
- `css/styles.css` — `.analytics-reset-btn.has-filter` warm accent styles
- `tournament.html` — `analyticsResetBtn` id on Reset button; `analyticsImportWarning` div in import modal

---

## **v5.0.12** — Clear Sight Lines (2026-04-16)

### Header

- **Active Tournament label** — replaced absolute-positioning hack with proper flex layout. Wide viewports show tournament name and date stacked next to the clock. Narrow viewports (below 1024px) show a centered row with full "Active Tournament:" prefix. Fixes overlap that blocked nav buttons at mid-range widths.

### Analytics

- **Table redesign** — dark header bar (`#1f2937`) with white uppercase labels, sticky on scroll. Rows float as rounded cards with `border-spacing`, subtle shadows, and visible alternating stripes (`#eef0f4`). Hover lifts rows with stronger shadow. Top-16 leaderboard rows use stronger green tones.
- **Badge cells** — Rank (#) and Points columns in the Leaderboard render as badge cells: rounded, tinted inner `<div>` with margin, creating real visual separation from the surrounding row. Works across white, striped, and green rows.
- **Typography** — player names, tournament names, and match tournament links render at 15px bold. Match scores render bold at 15px. Tournament name column expands to fill available width, pushing data columns to the right edge.
- **Pagination buttons** — rows-per-page and page navigation buttons match the Lens preset button style.

### Tournament Leaderboard

- **Visual refresh** — Player Registration leaderboard and statistics modal adopt the same table styling: dark header, row separation, rounded corners, alternating stripes. Compact `8px` padding. Player names and points at 15px bold.
- **Layout fix** — removed legacy hidden column that caused horizontal overflow on all pages.

### Files changed

- `css/styles.css` — table redesign, header layout: dual-layout tournament status, badge cell styles, pagination button styles
- `js/tournament-management.js` — `updateTournamentStatus()` populates both wide and narrow header elements
- `js/newton-history.js` — badge cell class on rank/points columns, 15px bold names/results, tournament column width
- `js/newton-table.js` — `cellClass` support for column config, removed wrapping overflow div
- `js/results-config.js` — removed hidden empty column from leaderboard rows
- `tournament.html` — Active Tournament label moved to `.header-right` with narrow duplicate; removed hidden empty column

---

## **v5.0.11** — (2026-04-15)

### Analytics

- **Import confirmation modal** — replaces the plain confirm() dialog with a styled modal showing tournament name, date, player count, match count, format, and a list of what will be imported.
- **Delete modal shows date** — tournament name now includes the date in parentheses for disambiguation. Confirmation prompt shows the exact name to type.
- **Import Register in analytics mode** — no longer hidden. Register import is safe (merge, not overwrite) and enables full Analytics without requiring tournament JSONs on disk.
- **Half-year presets** — two buttons in the Lens bar for quick date scoping: current half-year and previous half-year. Labels auto-compute from today's date (e.g. 2026H1, 2025H2).
- **Leaderboard sort direction** — first click on a column header now sorts descending (highest first) for all numeric columns. Exception: Best Leg sorts ascending (fewest darts first). Player name sorts alphabetically. Powered by `defaultDir` property on NewtonTable columns.
- **Matches dashboard card** — the Matches stat card links to a flat all-matches view showing every completed match across the current scope, with columns for Match, Player 1, Player 2, Result, Points, Tournament, Date, and Type. Winners bolded.
- **Register sub-tabs with breadcrumb navigation** — the Register view has two sub-tabs: Tournaments and Matches. Drilling into a tournament or match extends the tab bar into a breadcrumb trail: `Tournaments / Måndagscup (2026-03-09) / FS-4-1`. Each segment is clickable. Tournament date shown for disambiguation. Replaces the old back-button navigation.
- **Match detail header** — shows date and match ID (`2026-03-09 · **FS-4-1**`) instead of full timestamp.

### Bugfix

- **Match detail lookup** — fixed stale closure where clicking a match in the per-tournament match list would fail with "Match record not found" after viewing more than one tournament in the same session. The table's row click handler captured the tournament ID from the first call, ignoring subsequent tournaments.

### Files changed

- `tournament.html` — register sub-tabs, breadcrumb bar, import confirmation modal
- `js/newton-history.js` — matches view, breadcrumb navigation, `switchRegisterTab()`, match detail bugfix, half-year presets, import modal, leaderboard sort direction
- `css/styles.css` — breadcrumb styles, sub-tab styles

---

## **v5.0.10** — The Full Picture (2026-04-15)

### Analytics-only mode

- **NEWTON_MODE=analytics** — Docker environment variable. Hides Tournament Setup, Player Registration, Tournament Bracket, and Chalker tabs. Shows only Analytics and a limited Global Settings (Branding + Point Values). Header set by PHP — no flash.
- **CSS-driven mode system** — PHP sets `<body class="mode-analytics">`, CSS handles all visibility. No JavaScript display:none, no flash on load. Clean platform for future modes.
- **Read-only bracket view** — in analytics mode, "Bracket" button on tournament list and "View Bracket" in match list loads the tournament JSON from disk via API. Renders bracket read-only using `currentTournament` with `_analyticsPreview` guard — `saveTournamentOnly()` skips analytics previews. Bracket ID matching via tournament ID (with name+date fallback). "Back to Analytics" clears `currentTournament` and globals.
- **Import Register hidden** — analytics mode hides the Import Register button. Data comes exclusively from disk (shared tournaments).
- **Delete buttons respect settings** — Analytics register delete button only shown when "Allow deleting tournaments" is enabled in Server Settings. In analytics mode, Server Settings is inaccessible, so deletion is impossible by design.
- **Help system disabled** — `initializeHelpSystem()` and `suggestHelp()` skip in analytics mode. No "No active tournament" notifications.
- **Font preloading** — Insignia and Manrope fonts preloaded in tournament.html, eliminating font swap flash on load.
- **Auto-navigate to Analytics** — CSS hides Setup page and shows Analytics immediately; JS confirms via `showPage('history')`.

### Docker Environment Variables

| Variable | Values | Default | Description |
|---|---|---|---|
| `NEWTON_MODE` | `full`, `analytics` | `full` | App mode. `analytics` hides tournament management tabs, shows only Analytics and limited Global Settings. |
| `NEWTON_LANDING_PAGE` | `true`, `false` | — | Enables the landing page at `/`. Existing variable. |
| `NEWTON_DEMO_MODE` | `true`, `false` | `false` | Shows demo banner. Existing variable. |
| `NEWTON_GITHUB_URL` | URL | `https://github.com/skrodahl/NewTon` | GitHub link in UI. Existing variable. |
| `NEWTON_BASE_URL` | URL | — | Base URL for canonical/OG tags. Existing variable. |

### Global Settings

- **Server Settings separated** — Server ID, auto-backup, shared tournament delete, and remote backup settings moved from the User Interface section into their own "Server Settings" pane. Cleaner separation of concerns.
- **"Allow deleting tournaments"** — renamed from "Allow deleting shared tournaments". Now also controls delete button visibility in the Analytics register.

### Analytics

- **Dashboard entry point** — clicking "Analytics" on a tournament (from Recent Tournaments or the podium) now always opens the Dashboard tab, not the last visited tab.
- **Import Tournament button** — in the Analytics header. Import a single tournament JSON file directly into the Analytics register. Works without the API — direct file to IndexedDB. Available in both full and analytics mode.
- **Shared backfill function** — `NewtonDB.backfillTournament(t, config)` extracted to newton-db.js. Single source of truth for importing tournament data into IndexedDB. Used by file import, localStorage backfill ("+ Analytics"), and auto-import from disk.
- **Auto-import from disk** — on every Analytics load, checks for shared tournaments on disk not yet in IndexedDB and imports them silently. One API call + ID checks. New tournaments appear selected.

### Chalker

- **Unlimited darts per leg** — new "Unlimited" option in Max Rounds (both Global Settings and Chalker). Uses value `100` (300 darts) — no special-case logic needed, all existing code works unchanged. QR payload stays within comfortable scanning limits. QR confirm display shows "Unlimited" for values >= 100.

---

## **v5.0.9** — Leader of the Pack (2026-04-14)

### UI

- **Rounded corners on buttons** — `border-radius: 6px` on `.btn`, `.btn-small`, `.player-list-item-actions button`, and `.cc-match-action-btn` globally. Buttons now visually distinct from input fields and panels, which remain flat.
- **Reset filter button** — clears text filter, resets date range, checks all tournaments, and resets scope to all.
- **Register "View →" buttons removed** — row click already navigates to the tournament. Delete button stays. Cleaner table, more space for data columns.
- **Date/time separator** — match timestamps now use middot separator: `2026-03-09 · 21:13`.

### Analytics

- **Unique player count fixed** — Dashboard "Players" card now deduplicates by lowercase trimmed name across tournaments instead of per-tournament player IDs. Drops inflated counts to actual unique players.
- **Points column in Register** — tournament list and match list both show achievement points. Tournament list includes ranking and attendance layers when toggled on.
- **Point mode toggle wired** — Original uses the frozen configSnapshot per tournament; Current uses today's Global Settings. Switching updates all views in real time, including match list and match detail.
- **Ranking and Attendance toggles** — two independent layer buttons alongside the Original/Current toggle. Ranking adds placement points (1st, 2nd, 3rd...). Attendance adds participation points per player. Both on by default — toggle off to drill down to pure achievement points. Tournament-level only; match views show achievement points.
- **Tournament Analytics button on podium** — one click from the celebration screen to Analytics scoped to the just-completed tournament.
- **Placements stored at finalization** — `tournament.placements` now written to IndexedDB at tournament close, enabling ranking point computation in Analytics.
- **Total Points dashboard card** — sums all points across scoped tournaments, respects point mode and layers. Clicks through to the Leaderboard.
- **Leaderboard** — cross-tournament player rankings with full stat breakdown: placement counts (1st through 7-8th), 180s, high outs, short legs, best checkout, best leg (fewest darts), three-dart average (Chalker matches only), matches won/lost, legs won/lost. All columns sortable. Rank and Points columns shaded. Top 16 players highlighted with muted green background (survives reordering). Respects point mode (Original/Current), Ranking, and Attendance toggles. Player dedup by lowercase trimmed name.

### Migration

- Tournaments imported into Analytics before this version do not have placement data in IndexedDB. To enable ranking points for these tournaments: delete from the Analytics register, then re-add via "+ Analytics" in Recent Tournaments.

---

## **v5.0.8** — Choose Your Lens (2026-04-13)

### Landing page

- **Chalker card closer** — "No shouting results across the room." Replaces "No walking results across the room." — more honest.
- **Setup card closer** — "Same steps, same setup, every week." Replaces "No configuration rabbit holes." — communicates consistency over snark.

### Analytics

- **Subtitle rewritten** — "Analytics lets you explore your tournament history. It never alters the underlying results." Replaces the placeholder text. Sets expectation and builds trust.
- **Register column reorder** — Format moved to last position. Tournament, Date, Players, Matches are the primary columns; Format is metadata.
- **Scope selector** — tournament selection that drives all analytics views. Three composable filters narrow the dataset: text filter (AND keyword match on tournament names), date range (inclusive from/to pickers), and per-tournament checkboxes. Each filter only narrows — they never widen. The scope is the intersection of all active filters.
- **Scope indicator** — green pill in the Analytics header shows "Viewing: N of M tournaments" with selection tags. Clickable to navigate to Register. Header checkbox in the tournament table toggles all visible rows.
- **Text filter** — instant keyword filtering on tournament names. Space-separated terms use AND logic. Controls table visibility; checkboxes set scope within the visible set. Persisted across reloads.
- **Date range filter** — from/to date pickers. Inclusive on both ends. Prefilled with the earliest and latest tournament dates in the register. Persisted across reloads.
- **Scope persistence** — scope, text filter, and date range all saved to localStorage. Stale tournament IDs are filtered out on restore.
- **Dashboard respects scope** — all stat cards (tournaments, matches, players, 180s, highest checkout, shortest leg) compute from the scoped tournament set.
- **Dirty flag recompute** — changing scope marks inactive views dirty; they recompute on next switch (lazy).
- **openAnalyticsForTournament() wired** — clicking "Analytics" on a tournament in My Tournaments sets the scope to that single tournament.
- **Cache invalidation** — import and delete operations reset the tournament cache and scope.

---

## **v5.0.7** — The Offline Authority in Darts (2026-04-13)

### Landing page

- **New hero messaging** — "The Offline Authority in Darts" tagline with three-beat subtitle: "Runs locally. Syncs across devices. Builds your venue's stats over time."
- **Story cards** — three bare-text cards between the hero and launch buttons: "Yours. Not theirs." / "Scan. Score. Done." / "Every match remembered."
- **In Action cards rewritten** — new titles, tighter body text, and italic closer lines for every card. Consistent voice: cheeky, confident, practical.
- **Meta tags updated** — description, Schema.org, Open Graph, and Twitter Card all carry the new messaging.

### Documentation

- **llms.txt** — full refresh: new messaging, Analytics section, Server Backup section, updated privacy model, expanded architecture notes and AI tips.
- **architecture.html** — updated Offline-First section (IndexedDB, opt-in server backup); updated single-user limitation (acknowledges shared tournaments).
- **Design system** — feature card restyled (left border accent replaces gradient stripe); dark card and bare card variants added.
- **REST API docs** — relay endpoint, backup modal, auto-backup, Global Settings config table added to both `Docs/REST_API.md` and `rest-api.html`.

---

## **v5.0.6** — Because Backups Shouldn't Be Homework (2026-04-12)

### Server backup

- **Automatic backup on tournament completion** — opt-in via Global Settings. When enabled, the tournament JSON is automatically saved to the local server's `/tournaments/` directory at finalization. Backed up tournaments are available from any computer connecting to the server.
- **Remote backup** — configurable remote server URL with optional basic auth credentials in Global Settings. When configured, auto-backup sends to both the local server and the remote server. Remote upload uses `api/relay.php` — PHP forwards the request server-side, bypassing CORS/preflight issues with authenticated endpoints.
- **"Backup to Server" button** — replaces the old "Upload to Server" file picker. Opens a modal with source choice (Active tournament / From file) and destination info. Uploads to all configured destinations (local + remote).
- **`api/relay.php`** — new endpoint. Accepts a remote URL, optional credentials, and a tournament payload. Forwards via PHP curl with basic auth. Validates URL, passes through the remote server's response.
- **Global Settings — Server section** — Server ID (read-only display), auto-backup checkbox, remote server URL, username, and password fields. All persisted in `config.server`.

### Shared Tournaments

- **Collapsed by default** — Shared Tournaments section starts collapsed with a count header ("Shared Tournaments (13)"). Click "Show All" to expand, "Collapse" to close. My Tournaments stays front and centre.
- **Import / Re-import labels** — tournaments not in localStorage show "Import", tournaments already present show "Re-import".

### Navigation

- **Tab reorder** — Analytics now comes before Global Settings in the navigation bar. Flow: Tournament Setup → Player Registration → Tournament Bracket → Analytics → Global Settings → Chalker.

### Analytics — polish

- **Date format** — all dates in Analytics now use YYYY-MM-DD, consistent with the rest of the app. Match detail timestamps show YYYY-MM-DD HH:MM.
- **Format label** — match list header spells out "Double Elimination" / "Single Elimination" instead of "DE" / "SE". Tournament table keeps the compact abbreviation.
- **Backfill format field** — backfilled matches now include the best-of value from the localStorage match object, enabling the "Best of N" badge on match details.
- **Match detail badges** — "Best of N" and "Manual/Chalker" type badges right-aligned in match header.

### Files changed

- `api/relay.php` — new relay endpoint for remote server uploads
- `js/tournament-management.js` — `buildTournamentPayload()`, `uploadToServer()`, `autoUploadTournament()`, `showUploadModal()`, shared tournament collapse
- `js/clean-match-progression.js` — auto-backup hook at tournament finalization
- `js/results-config.js` — new `config.server` fields
- `js/newton-history.js` — YYYY-MM-DD date formatting, format label expansion
- `tournament.html` — Backup to Server modal, Server Settings UI, nav reorder
- `css/styles.css` — modal ESC support

---

## **v5.0.5** — Read Between the Lines (2026-04-11)

### Analytics — table utility (`newton-table.js`)

- **Reusable sortable/paginated table component**: `NewtonTable.create()` accepts column definitions (key, label, width, align, sortable, render, sortValue), data array, and callbacks. Handles sorting, pagination, and rendering. Every future tabular view gets these features for free.
- **Sortable columns**: click header to sort ascending, click again to reverse. Visual indicator (▲/▼) on active column.
- **Pagination**: rows-per-page selector (10 / 25 / 50 / All) with Prev/Next navig