Changelog

Release history and notable changes for Curling Stats.


# Changelog

All notable changes to Curling Stats are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [2.6.8] - 2026-03-18

### Added

- **Score V2: rotation persists across rubric ↔ placement tab switches**
  The 180° diagram rotation toggle on the placement panel now survives switching to the rubric tab and back. Previously, navigating to the rubric unmounted `PlacementPanel` and reset the local `rotated` state to `false`. The rotation flag (`placementRotated`) is now held in the Zustand scoring store UI state (non-persisted) so the rotation is maintained for the duration of the scoring session.

- **Score V2: thinking-time "Position" column now shows curler name**
  When lineup data is available for the current end, each row in the Thinking Time pair table now displays the curler's name next to their position label (e.g. `Skip · Alex Smith`). The name is resolved via the existing lineup-resolution logic. Rows where the curler cannot be determined (missing lineup entry, out-of-range shot number in a partial end) show the position label alone with no fallback `?`.

- **Score V2: sparkline segments colored by shooting percentile**
  The shooting-percentage sparkline now draws each segment in a color reflecting the end-of-segment percentile: red for below 50%, yellow for 50–75%, and green above 75%. Each segment is rendered as an independent `<line>` element so the color transitions are per-segment rather than a single stroke for the whole line. Segments adjacent to null-percentage ends are skipped (no misleading color bridges across missing data). The same coloring applies to the opponent line when opponent tracking is enabled.

### Fixed

- **Score V2 / Replay: ghost stone appearing at intermediate placement clicks**
  When a user clicked a stone to an intermediate position (e.g. the guard area) and then clicked again to place it in its final location during the same shot, a ghost outline appeared at the intermediate position rather than at the stone's true pre-shot location. The fix is two-pronged:
  1. `computeBoardStatesFromShots()` now collapses multiple `moved[]` entries for the same `stone_ref` within a single shot into exactly one move. The `from` (ghost) coordinate is always captured from the board state before any moves are applied for that shot.
  2. `PlacementPanel.handleDiagramClick` replaces rather than appends the `moved[]` entry for the selected stone reference, so duplicate intermediate positions are never saved in the first place.

- **Replay: removed stones spread along the backboard instead of stacking**
  Stones removed from play on the `/replay` page previously all rendered at the same coordinate, stacking on top of each other. The replay view now uses the same `getRemovedStonePositions()` + `isInRemovedStonesZone()` logic as the `/score-v2` placement panel to spread removed stones along the backboard (our team on the left, opponent on the right, wrapping to a second row after 5 stones per team).

- **Replay: diagram container widened to prevent horizontal scrolling at 200%**
  The maximum width of the house-diagram container and its controls on the `/replay` page has been increased from `max-w-lg` to `max-w-2xl` so that a `diagramSize` of 640 px (scale 2×) fits without triggering a horizontal scrollbar.

- **Replay: shot-info overlay card changed to 50% opacity**
  The current-shot and "before first shot" overlay banners inside the replay diagram now use `bg-background/50` (was `bg-background/90`) so they are visually lighter and less obstructive.

---

## [2.6.7] - 2026-03-18

### Added

- **Score V2: YouTube timestamped links in thinking time**
  When a game has a YouTube stream URL configured, the Thinking Time panel in Score V2 now displays a "YouTube links" section below the pair table. Each pair shows a Start and Stop link that opens the YouTube stream at the exact timestamp, matching the behavior of the original Score page. Links are only shown for pairs with valid timestamps and are skipped if the timestamp would produce an invalid offset.

- **House diagram: 180-degree rotation toggle**
  The shot placement panel and the replay view now include a rotate button (⟳) in the diagram control bar. Clicking it flips the house diagram 180 degrees so the hack is at the top and the hog line at the bottom — useful for matching the perspective from behind the house. Click again to restore the default orientation. The button shows a highlighted state when rotation is active and carries an `aria-pressed` attribute for accessibility. Implemented via CSS transform so click-to-place and hit-testing continue to work correctly.

### Fixed

- **Score V2: crash when pasting thinking-time timestamps (React error #185)**
  Pasting timestamps in the Thinking Time panel for End 1 triggered a "Maximum update depth exceeded" crash. The root cause was `useEndThinkingTimeSummary` returning a freshly-allocated object on every Zustand selector call, causing an infinite re-render loop. The hook has been refactored to use `useShallow` + `useMemo` (the same pattern used by `useAllEndPercentages`) so the derived object is only recomputed when the underlying store data changes.

- **Score V2: defensive guard on last-end detection**
  When the store's `ends` array is empty during early hydration, `Math.max(...[])` evaluated to `-Infinity` and was passed as `lastEndNumber` to `ThinkingTimePanel`, causing undefined behavior in the final-end confirmation logic. This is now guarded to fall back to the current end number when no ends are loaded.

---

## [2.6.6] - 2026-03-18

### Added

- **Replay: zoom controls**
  The replay diagram now includes zoom controls (100% / 150% / 200%) matching the shot placement panel. Use the − and + buttons next to the navigation controls to zoom in or out; the diagram scrolls when zoomed beyond the container width.

- **Replay: cross-end navigation**
  When more than one end has placed shots, the Previous and Next buttons now navigate across ends. Pressing Next on the last shot of an end automatically loads the first shot of the next end. Pressing Previous at the beginning of an end (before the first shot) loads the previous end and jumps to its last shot. The Play animation also crosses ends automatically. The Previous button is disabled only on the very first shot of the first end, and the Next button is disabled only on the last shot of the last end.

### Fixed

- **Security: RLS enabled on `coach_goal_check_ins`**
  The `coach_goal_check_ins` table was publicly accessible via PostgREST because Row Level Security had not been enabled. A new migration enables RLS and adds SELECT, INSERT, UPDATE, and DELETE policies that allow access only when the related `coach_goals` row is accessible to the current user (leveraging the existing entity-scoped RLS on `coach_goals`).

### Changed

- **Shot placement: removed-stones wrap to a second row**
  When more than 5 rocks of one color have been removed from play, the removed stones now wrap to a second row rather than crossing the centre line into the opposing team's half. Each row holds up to 5 stones; a second row appears above the first within the existing backboard zone.

- **Dashboard: get-started card more readable in light mode**
  The getting-started tips card on the dashboard now uses a near-opaque card background (`bg-card/90`) in light mode instead of the very transparent `bg-primary/5`, making the text clearly readable over the background image.

- **Accessibility: link color darkened in light mode**
  The base link color in light mode has changed from bright yellow (`#ffcc00`) to a dark amber (`#7a5c00`), which achieves WCAG AA contrast (≥ 4.5:1) against the light background. Dark-mode link color is unchanged (`var(--primary)`).

---

## [2.6.5] - 2026-03-17

### Fixed

- **Replay: dark mode sheet background**
  The replay page diagram (`/games/[id]/replay`) now shows a white background between the sidelines in dark mode, matching the behavior already present on the score-v2 placement panel. Root cause: `ReplayView` was not passing `sheetOnlyWhiteInDarkMode` to `HouseDiagram`. The same `MutationObserver`-based dark mode detection used in the placement panel is now applied to the replay view.

- **Score V2: opponent rock placements lost when navigating away and back**
  After placing rocks for both teams and navigating to `/replay` then returning to `/score-v2`, all opponent rock placements were lost. Root cause: `ScoringViewV2` called `reset()` before `hydrate()` on re-mount, which triggered the Zustand persist middleware to overwrite the `localStorage` cache with an empty store before the dirty shots (rocks placed within the last 500ms) could be flushed to the server. Additionally, `setPositionOutcome` marks a shot dirty but does not immediately enqueue it for syncing — only navigation to another shot triggers the queue — so the final placement before leaving the page was never saved. The fix adds two safeguards: (1) a `commitAllDirtyAndFlush` store action that enqueues every in-memory dirty shot (not just the currently active one) and flushes them before the store is reset; (2) an unmount cleanup in `ScoringViewV2` that cancels the debounce timer and calls `commitAllDirtyAndFlush` so shots placed right before a client-side navigation are persisted to the server.

- **Score V2: end score boxes — cannot type a number; only arrows work**
  The end score input boxes in the end panel header now support free-form keyboard entry. The inputs use `type="text"` with `inputMode="numeric"` so both desktop and mobile users can type a number directly; non-numeric characters are filtered out and values outside 0–8 are rejected. Previously the `type="number"` inputs' native spinner behavior was the only reliable way to change the value in some browsers.

### Changed

- **Score V2: end scores enforce one team scoring per end**
  In curling, only one team can score in an end (or neither — a blank end). The end score header now enforces this client-side: typing a score greater than zero for one team automatically sets the other team's score to zero. A server-side refine in `endScoresUpdateSchema` also rejects any payload where both `team_score > 0` and `opponent_score > 0`.

- **Landing page: card more transparent**
  The unauthenticated login card at `/` is now more transparent (`bg-background/40`, border `border-border/40`) to reveal more of the background image. The dark gradient overlay is also lighter (from `0.4→0.65` opacity instead of `0.6→0.9`) so the curling sheet photograph is more visible.

- **Dashboard: background image**
  The authenticated dashboard (and all pages within it) now use the same curling splash image (`/images/landing-splash.png`) as the landing page for a consistent visual theme. A semi-transparent overlay adapts to the current theme: a white wash in light mode and a dark wash in dark mode to keep all sidebar, header, and page text fully readable.

---

## [2.6.4] - 2026-03-17

### Fixed

- **Score V2: initial navigation jumps to first unscored shot**
  When a game was opened in score-v2, the page was incorrectly navigating to the **last** end that had an unscored slot (e.g. End 8) rather than the **first** one. Root cause: `findFirstUnscoredPosition` (formerly `findLatestUnscoredPosition`) sorted ends in descending order. The sort is now ascending so the page always opens on the earliest unscored position in the game. The **First Unscored** button and Home key shortcut were already correct (they use a separate ascending-sort path).

- **Score V2: "Save failed — retry" on placement saves with moved stones**
  Saving a shot with `position_outcome` that included moved stones (`moved[].stone_ref`) was incorrectly rejected by the server-side schema with "Invalid position_outcome". Root cause: `createShotSchema` validated `stone_ref` values using the **DB** shot number (1–8), but stone refs in the store use **interleaved** shot numbers (1–16). For example, interleaved shot 5 maps to DB shot 3; a stone ref `shot_3` is a valid prior interleaved shot but was rejected because the DB-number-based schema allowed refs only up to shot 2. The fix removes `stone_ref` validation from `createShotSchema` (leaving only coordinate-bounds checking), and defers it entirely to the existing interleaved-aware re-validation block in `createOrUpdateShot` that already uses the correct interleaved shot number.

- **Score V2: "Save & Next" unclickable after placement adds moved stone**
  After scoring a shot and placing it (including moving a stone), the Save & Next button could appear stuck if the auto-flush sent a stale mutation snapshot (one captured before placement was added). The flush now always reads the **latest** shot state from the Zustand store map rather than the snapshot captured at enqueue time, so any placement added between the initial save-and-advance and the flush is included in the save.

---

## [2.6.3] - 2026-03-17

### Changed

- **Landing page: full-screen background image**
  The unauthenticated root page (`/`) now displays the Curling Stats splash artwork as a full-screen background image. A dark gradient overlay ensures the heading, description, and action buttons (Sign in / Sign up / View demo dashboard) remain readable in both light and dark themes. Content is presented in a centered, frosted-glass card (`backdrop-blur` + semi-transparent background). The background image is marked decorative so screen readers focus on the auth actions. Authenticated users continue to be redirected to `/dashboard` immediately, so the new landing UI is only visible to unauthenticated visitors.

---

## [2.6.2] - 2026-03-17

### Added

- **Privacy Policy and Terms of Service pages**  
  Dedicated **Privacy Policy** and **Terms of Service** pages are now available and linked from the app chrome, consolidating legal content that previously lived only in external docs.

- **Legal overview and webhook documentation**  
  Internal and customer-facing documentation now covers the app's legal pages and webhook behavior (what events are emitted, payload shape, and reliability expectations), so administrators and integrators have a single place to review obligations and integration details.

### Changed

- **Score V2: feature flag moved to admin setting**  
  The Score V2 rollout flag has moved from the `NEXT_PUBLIC_SCORE_V2_ENABLED` environment variable to a runtime-configurable `score_v2_enabled` setting in the admin console (App settings). Admins can enable or disable Score V2 routing without redeploying, following the same pattern as `shot_placement_enabled`.

---

## [2.6.1] - 2026-03-16

### Added

- **Score V2 Phase 3: thinking time in end-panel** – When the game has "track thinking time" enabled, the v2 score page shows a **collapsible thinking time** section in the end panel (keyboard shortcut **T** to toggle). Manual entry: paste tab-separated start/stop timestamps (one pair per line) or edit pairs in the table; **Apply** and **Clear** per end. Live stopwatch mode is planned (UI shows "coming soon"). Data syncs via existing `updateEndThinkingTime` / `clearEndThinkingTime` server actions.
- **Score V2 Phase 3: multi-scorer awareness badge** – A badge in the v2 scoring header shows when other users are scoring the same game (invited scorers or collaborators), so you're aware of multi-scorer activity.
- **Score V2 Phase 3: inline analytics sparkline** – A slim **sparkline** strip below the end tab bar shows shooting percentage trend by end, updating as you score.

---

## [2.6.0] - 2026-03-15

### Added

- **Score V2 Phase 2: quick-entry grid** – Full grid-based entry via `quick-shot-row.tsx` and `quick-end-panel.tsx` with large touch targets, auto-advance on score tap, and category quick-select for rink-side (coach) use.
- **Score V2 Phase 2: three-mode scoring toggle** – Header toggle offers **Detailed** ↔ **Compact** ↔ **Quick Grid**. Mode is persisted to user preferences via the existing preferences server action; default remains detailed.
- **Score V2 Phase 2: shot placement panel** – `placement-panel.tsx` and `placement-controls.tsx` are lazy-loaded via `dynamic()`. On desktop the panel appears as a side panel (~40% width); on mobile as a bottom sheet. Supports Simple (delivered only) and Full (delivered + moved) modes. **Burned** button in placement controls; **B** keyboard shortcut sets burned.
- **Score V2 Phase 2: context panel tabs** – When both rubric and placement are enabled, `context-panel.tsx` shows tabs to switch between rubric and placement content.
- **Score V2 Phase 2: doubles adaptation** – Dynamic shot count (10 shots/end) in end panel; power play controls per end above the shot list; percentage calculations adjusted for doubles.
- **Score V2 Phase 2: keyboard shortcut cheat sheet** – `shortcuts-modal.tsx` lists all shortcuts in a two-column table. Opened by **?** key or the **?** button in the scoring view header.
- **Score V2 Phase 2: haptic feedback** – `navigator.vibrate()` on successful save and on undo for mobile devices to improve rink-side feedback.
- **Score V2 Phase 2: responsive layout polish** – Tablet and phone breakpoint testing and refinement; landscape tablet optimization when context panel is absent (wider end panel with comfortable padding).

### Changed

- **Score V2: scoring mode preference** – `compactMode` has been replaced by `scoringMode` with values `"detailed"` | `"compact"` | `"quick"`. Existing localStorage entries using `compactMode` are migrated on hydrate so saved preferences are preserved.

### Fixed

- **Doubles thinking time position mapping** – Corrected the doubles position mapping so Position 1 throws the first and last rock per end and Position 2 throws the middle three, matching World Curling Federation mixed doubles rules. Thinking time pair-to-position groupings and user-facing documentation were updated accordingly.

---

## [2.5.1] - 2026-03-15

### Added

- **Score V2: new scoring page (Phase 1 MVP)** – A ground-up rebuilt scoring page is now available at `/games/[id]/score-v2` (authenticated) and `/t/[slug]/games/[gameId]/score-v2` (public read-only). Phase 1 targets the at-home audience reviewing video: detailed entry, keyboard-first, dynamic score scale, and inline rubric. Controlled by the `NEXT_PUBLIC_SCORE_V2_ENABLED` feature flag (default off). Both `/score` and `/score-v2` routes remain active in parallel during rollout.
- **Score V2: local-first Zustand store** – All mutations write to the Zustand store immediately; server actions (`createOrUpdateShot`, `updateEndScores`, `addEnd`, `undoLastShot`) are called asynchronously in the background. State is persisted to `localStorage` (keyed by `gameId`) on every field change — no debounce — so no data is lost on browser crashes or screen locks. On next page load, a hash-based comparison detects whether server data has changed since the last visit: if unchanged, dirty local shots are merged back; if changed, stale localStorage is discarded and an inline toast alerts the user.
- **Score V2: keyboard-first entry** – Full keyboard shortcut table: `H` / `D` / `G` select the first Hit / Draw / Guard type (repeated presses cycle within the category), `I` / `O` set turn in/out, `0`–`9` set score (capped at `scaleMax`), `X` toggles exclude, `Enter` saves and advances to the next unscored slot, `U` undoes the last shot, arrow keys navigate between ends and shots within an end, `N` focuses the notes field, `Escape` blurs notes, `Home` jumps to the first unscored slot across all ends. All shortcuts are inactive in read-only mode.
- **Score V2: dynamic score scale** – Score input renders 0 through `scaleMax` (any value 1–10) buttons plus X. Score colors use a proportional gradient regardless of scale: 0 = red, 0.25 × N = orange, 0.5 × N = yellow, 0.75 × N = lime, N = green. All percentage calculations and rubric display adapt dynamically to the game's score scale.
- **Score V2: compact mode** – A header toggle switches between **Detailed** (default, at-home) and **Compact** (rink-side) modes. Compact mode collapses shot type chips to three large category buttons (Guard / Draw / Hit), hides the notes field, and auto-advances to the next shot on score tap — no explicit "Save & Next" needed. Mode preference is persisted via the existing preferences server action.
- **Score V2: inline rubric panel** – When a rubric exists for the game's score scale, a rubric panel appears to the right of the shot list on desktop (≥ 1024 px) and as a collapsible accordion below the shot form on mobile (< 768 px). The panel filters to show descriptions for each score level (0–`scaleMax`) matching the currently selected shot type's category, and updates reactively as the shot type changes. When no shot type is selected, all categories and their general descriptions are shown.
- **Score V2: percentage badges with color-blind accessibility** – Shooting percentage is displayed as a colored badge with the numeric value always visible. Color coding: red < 50%, yellow 50–70%, green > 70%. A secondary non-color indicator accompanies each threshold — `+` / checkmark (> 70%), `~` / dash (50–70%), `!` / warning icon (< 50%) — so the badge is interpretable without color vision. All color/text combinations meet WCAG 2.1 AA contrast.
- **Score V2: background sync with exponential backoff** – A debounced `flushSync` action (500 ms) processes the pending mutation queue. On failure, retries with exponential backoff: 1 s → 2 s → 4 s. After 3 failed attempts the shot is marked as error and a "Save failed — click to retry" sync indicator appears (clicking calls `flushSync` immediately). Success clears the pending mutation and marks the shot as synced.
- **Score V2: end tab bar** – Horizontal scrollable tab bar with one tab per end. Each tab shows the end number, a hammer dot in the hammer team's rock color, and the end's shooting percentage (color-coded via `getScoreColor`). A `+` tab appends a new end via the existing `addEnd` server action. The active tab scrolls into view automatically on mount and on end change.
- **Score V2: interleaved shot number model** – Display shot numbers are interleaved (1–16 for standard, 1–10 for doubles) and serve as the Zustand store's primary key (`${endNumber}-${shotNumber}`). Odd slots = non-hammer, even slots = hammer for both formats. `dbShotToStoreShot` maps DB per-team shot numbers into interleaved display numbers on hydration; `storeShotToDbShot` maps back when calling `createOrUpdateShot`. Property-based roundtrip tests verify bijection.
- **Score V2: rubric definition parser** – `lib/rubrics/parse-scoring-definition.ts` adds `parseScoringDefinition` (parses free-text `scoring_definition` DB rows into per-score-level `RubricEntry` items, with a raw-text fallback) and `adaptDbRubricToV2` (converts a DB `RubricWithEntries` to the v2 `Rubric` type consumed by the store and rubric panel). Does not affect the v1 score page rubric code path.
- **Score V2: feature flag and navigation** – `NEXT_PUBLIC_SCORE_V2_ENABLED=true` (default false in `.env.local.example`) activates v2 routing. When on: the **Score game** button on the game detail page routes to `/games/[id]/score-v2`; the **Shot-by-shot scoring** link on the public game page routes to `/t/[slug]/games/[gameId]/score-v2`; the "Game already has scoring" dialog's **Do my own scoring** action routes to `/score-v2`. The v1 `/score` route remains fully functional for all users regardless of flag state.
- **Zustand** – Added as the first global client-side state management library in the codebase (`npm install zustand`). Used exclusively by the score-v2 component tree.

### Changed

- **Score game button** – `ScoreGameLink` component now reads `NEXT_PUBLIC_SCORE_V2_ENABLED` and links to `/score-v2` when the flag is on, otherwise `/score`. The "Game already has scoring" dialog's "Do my own scoring" action respects the same flag.
- **Public game page — Shot-by-shot scoring link** – The "Shot-by-shot scoring" button on the public game page (`/t/[slug]/games/[gameId]`) now routes to `/score-v2` when `NEXT_PUBLIC_SCORE_V2_ENABLED` is on, otherwise `/score`.

---

## [2.4.1] - 2026-03-13

### Added

- **Doubles: Power play & placement side card** – For **Doubles** games, a dedicated **Power play & placement side** card appears **under the club scoreboard** on the game detail page. Set which team used power play each end and which side of the sheet the pre-placed rocks were set (Left/Right). Placement side is required when power play is used. Per-row saving with loading and error feedback. Affects replay and stats.
- **Doubles: Power play on score page** – On the shot-by-shot score page, when the game is **Doubles**, a compact **Power play (this end)** block appears above the shot list for the **current end only**. Set or correct power play and placement side while scoring; the scoreboard card remains the primary place.
- **Placement side required when power play used** – Backend validation now requires **Placement side** (Left or Right) when power play is used for an end; saves without placement side are rejected with a clear message.

### Fixed

- ** fixed git issues
- **Doubles E1-S1: moving preplaced stones** – Moving a pre-placed stone (pre_1 or pre_2) out of play on the placement diagram for **Doubles** shot E1-S1 no longer returns "Invalid position_outcome (bounds or stone_ref)." Validation now allows `pre_1` and `pre_2` in `moved[].stone_ref` when the game format is doubles (maxShotNumber 5).

### Changed

- **Global design system** – The app now uses the design system from Documents/internal/design: neutral (oklch) palette, **IBM Plex Sans** for UI and body text and **Geist Mono** for monospace (scores, code), design radius and shadow tokens, and light/dark theme with `color-scheme` for form controls and scrollbars. Rock-colored elements (shot buttons, house diagram stones) are unchanged and intentionally excluded from theme overrides.
- **Doubles shot list: all shots per end** – The middle (Shot placement) card on the score page now shows **10 shots per end** for Doubles (both teams), with the same display rules as standard (alternating team colors, E1-S1 … E1-S10). Previously only 5 (one team) were shown.
- **Single scoring route** – **Score game** now opens the **three-card layout** (form, shot list, place rocks) at **/games/[id]/score**. The former **/games/[id]/score2** route is removed; visiting **/games/[id]/score2** redirects to **/games/[id]/score** with the same query string. The "Try layout 2" link on the (read-only) public score view is removed.
- **Power play in Edit End hidden for Doubles** – When the dedicated Power play & placement side card is shown (Doubles), the power play and placement side controls are no longer duplicated in the Edit End row; set power play from the card only.

---

## [2.3.8] - 2026-03-13

### Fixed

- **Score2 rubric when placement off** – When Shot placement is **Off** on score2, the rubric card now updates correctly when you select a shot type in the form (previously it stayed on "Select a shot type…").
- **Score2 Save shot and Undo last shot** – On the left card, **Save shot** and **Undo last shot** are now the same dimensions and sit next to each other in one row.
- **Score2 one-team shot progression** – When Shot placement is **Off** and you are only scoring your team (track opponent scores off), saving a shot now advances to **your team's next shot** (e.g. E1-S1 → E1-S2 as your second shot). With placement on or when scoring both teams, E1-S2 remains the opponent's first rock.
- **Position only when placement off** – When Shot placement is **Off**, **Position only** is no longer shown in the shot type dropdown (it is only available when placement is on).
- **Score2 Next shot / Done button state** – After scoring and placing E1-S1–S3, **Save shot** and **Next Shot** on the left are now enabled when you have placed a rock for E1-S4 (even before clicking Done). When you click **Done** and advance to E1-S5, the right card's **Done** and **Next shot** buttons now work correctly (placement state resets when the slot changes).
- **Shots table shot_number constraint** – The database check constraint on `shots.shot_number` now allows **1–16** for standard games so an end can have up to 16 rocks (both teams) when using placement. Previously inserting a 9th rock triggered "shots_shot_number_check" and failed.

---

## [2.3.7] - 2026-03-12

### Added

- **Optimistic placement on score2** – When you save a shot placement and advance to the next shot (e.g. E1-S2 to E1-S3) on score2 in full placement mode, the just-placed stone now appears on the diagram immediately without waiting for the server refresh. The board state merges the saved placement optimistically so the next slot’s diagram shows the previous stone.

### Changed

- **Score2 diagram and placement** – **Removed-stones label:** The visible text "Stones removed from play (e.g. hogged, burned) appear here." was removed from the house diagram; the removed-stones zone keeps an aria-label for accessibility. **Zoom:** The zoom slider appears only in the top bar on score2; it is hidden inside the Place rocks card when zoom is controlled from the parent. **Diagram centering:** When the placement diagram is zoomed in, it is centered in its card with overflow scroll. **Yellow buttons:** Rock-colored buttons use black text for yellow (and similar hue) team colors via a dedicated hue rule (no luminance-based fix); other colors use the existing contrasting text. **Dark mode diagram:** In dark mode, only the sheet area (between the sidelines, backboard to hog line) has a white background; centre line, tee line, back line, and line labels (Hog, Back, Tee, Hack) use black so they remain visible. The full-diagram white wrapper was removed from the Place rocks card. **Position-only validation:** When saving a position-only shot from the Place rocks card, score is forced to 0 and turn to null. The shot form disables **Save shot** for position-only when no placement has been made and shows "Place the stone on the diagram first." if submit is attempted. **Shot placement card height:** The middle (Shot placement) and right (Place rocks) cards on score2 with placement on are limited in height to match the left (End X Shot Y) card; content scrolls inside. **Shot placement slot buttons:** The rock/slot buttons in the middle card (E1-S1 … E1-S16) are now 33% narrower (67% width).
- **Score2 center card** – Content in the center (Shot placement) card is centered; the shot list and help text align centered. On viewports under 768px the card remains readable with horizontal scroll where needed.
- **Linked Next Shot (score2)** – On the score2 layout, either **Next Shot** button (left card or Place rocks card) now saves both the shot form and the placement diagram when either has changes, then advances to the next shot. No lost edits when switching cards.
- **Ghost of moved stone on next shot** – When viewing shot N+1, a ghost outline appears at the previous position of any stone that was moved on shot N.
- **Removed stones at corners** – Removed stones (hogged/burned) are now placed at the backboard: first stone at the sideline corner, subsequent stones in a line toward the centerline (our team left, opponent right).
- **Dark mode sheet to backboard** – In dark mode, the white sheet background on the placement diagram now extends to the backboard (previously stopped at the back line).

---

## [2.3.6] - 2026-03-11

### Added

- **Score2 inline placement** – On the score2 page (`/games/[id]/score2`) with Shot placement On, the **Place rocks** card shows the placement diagram **inline** (no pop-out sheet). You can place stones directly in the right-hand card.
- **Next Shot (score2)** – When placement is inline on score2, the left card shows **Next Shot** instead of **Place Shot**. If the form has unsaved changes, Next Shot saves first then advances to the next slot; if the form is clean, it advances only. No silent overwrite of unsaved data.
- **Score2 Simple scoring toggle** – Score2 now has the same **Scoring mode** toggle as the main score page (Detailed | Simple | Both). In Simple or Both mode the simple tabular grid is shown; in Detailed or Both the shot form (and when placement On, the three-card layout) is shown.
- **Score2 placement-off layout** – When Shot placement is **Off** on score2, the **Place rocks** and **Shot placement** (slot list) cards are hidden. The page shows only the shot form and rubric in a two-column layout, matching the main score page in Detailed mode without placement.
- **Score2 rubric by shot type** – The rubric card on score2 now shows **description** and **scoring** for the currently selected shot type (same behavior as the main score page).
- **Next shot on Place rocks card** – The inline placement area on score2 shows a **Next shot** button (in addition to **Done**) that saves the current placement and advances to the next shot slot.
- **Default placement mode in Preferences** – In **Preferences → Shot placement diagram**, you can set **Default placement mode**: **Simple (delivered only)** or **Full (delivered + moved)**. Score and score2 use this as the initial placement mode.
- **Removed stones along backboard** – Stones that are removed from play (burned or placed in the removed-stones zone) are drawn **along the backboard**: our team left of center, opponent right of center, non-overlapping. Works for standard (16 shots/end) and doubles (10).

### Changed

- **Contrast on yellow buttons** – Luminance threshold for rock-colored buttons was lowered so **black text** is used on light yellows and other light rock colors for better readability (shot list, placement, and score entry buttons).
- **Score2 layout** – Column widths are now **40% / 15% / 45%** (form / shot list / Place rocks). Shot list and Place rocks cards share a minimum height so the list scrolls within the card. Slot buttons are as compact as possible.
- **Score2 help** – The links **What is shot position?** and **Which stones can be moved?** now open a **single help dialog** with both texts; either link opens the same content.
- **Zoom 100%–200% only** – Placement diagram zoom is now **100%, 150%, or 200%** only (250% and 300% removed). **Preferences** uses a **slider** for default placement diagram zoom (100%–200%). Stored values of 250% or 300% are treated as 200% when loading preferences.
- **Placement mode default Full** – Score2 and the main score page default placement mode to **Full** (or your saved preference). Preferences adds **Default placement mode** (Simple / Full).
- **Opponent shot default Position Only (one-team scoring)** – When you are only scoring your team (track opponent scores off) and you open an **opponent** shot with no existing data, the default shot type is **Position Only** instead of the first shot type in the list.
- **Simple mode: diagram shows only placed rock** – When placement mode is **Simple**, the placement diagram shows **only** the stone being placed for the current shot (no previous stones on the board).
- **Score2: tip and 16-stones line removed** – The tip "Focus a shot (e.g. E1-S1) then press P to open placement" and the line "16 stones = both teams…" were removed from the score2 shot list card.
- **Placement sheet Next button** – The placement **Next** button is now labeled **Next shot** for clarity.
- **Form save preserves position** – Saving shot type and score from the shot form (e.g. after placing a rock) now includes the existing **position_outcome** so placement data is not overwritten.

---

## [2.3.5] - 2026-03-11

### Added

- **Place Shot button** – In Detailed (and Both) mode with Shot placement On, a **Place Shot** button appears next to **Save shot**. Click it to save the current shot and open the placement diagram for that slot in one step (no auto-open when selecting a shot type).
- **Default placement diagram zoom** – In **Preferences**, a new **Shot placement diagram** card lets you set **Default placement diagram zoom** (100%, 150%, 200%, 250%, 300%). The placement sheet opens with this zoom level.
- **Score page layout 2** – An alternate layout is available at **/games/[id]/score2?mode=detailed**. When Shot placement is On, the page shows three cards side by side: **End X · Shot Y** (scoring form), **Shot placement** (compact vertical list of shots with help text), and **Place rocks** (preview diagram and Open placement). Zoom and placement mode selectors sit above the rubric. Use **Try layout 2** on the main score page (Detailed mode) to switch.
- **Opponent placement without lineup** – When you have Shot placement On but are only scoring your team (track opponent scores off), you can still **place opponent shots**. The app ensures placeholder opponent lineup rows exist when placement is on so you can record positions for both teams’ stones without completing the opponent lineup on the game page.
- **Opponent lineup placeholders** – When "Track shot-by-shot scores for the opponent" is on, the app always creates 2 or 4 opponent lineup rows at game create. If you leave names blank, placeholder labels (e.g. "Opponent Lead", "Opponent Second") are used so placement and scoring work; you can edit them later from the game page. New column `game_opponent_lineup.is_placeholder`; existing rows backfilled with `is_placeholder = false`. Shots linked to placeholder opponent rows are excluded from opponent-level statistics. Export/import include `is_placeholder` with round-trip and legacy default (false when missing).
- **Game update: opponent lineup** – When you enable "Track opponent scores" on an existing game that has no opponent lineup, the app creates 2 or 4 placeholder rows automatically.
- **Next button in placement** – In the placement sheet, **Next** saves the current placement and opens the diagram for the next shot (when placement is On in Detailed or Both mode). On the last shot of the last end, only **Done** is shown. If save fails, an error is shown and the sheet stays open without advancing. Keyboard: **Enter** saves (Next or Done as applicable).
- **Diagram zoom (placement)** – A **Zoom** control (100%, 150%, 200%, 250%, 300%) scales the placement diagram for more precise placement.

### Changed

- **Hammer and shot list colors** – When Shot placement is On, the shot list (E1–S1 … E8–S16) now shows **correct team colors by hammer**: the non-hammer team throws first (shot 1); the hammer team throws shots 2, 4, 6, 8. So when your team has hammer, your stones (e.g. E1–S2, E1–S4) use your rock color and opponent stones (E1–S1, E1–S3) use the opponent color. This matches standard curling throwing order.
- **Contrast on rock-colored buttons** – Team-colored shot buttons (placement list and Navigate) use a stricter luminance threshold so **black text** is used on light yellows and other light rock colors for better readability.
- **Placement sheet and zoom** – Selecting a shot type in the form **no longer** auto-opens the placement sheet. Use **Place Shot** (next to Save shot) to save and open placement. When the diagram zoom is above 100%, the placement sheet grows so the diagram fits without scrolling inside the window.
- **Shot validation** – Validation requires exactly one of `game_lineup_id` or `game_opponent_lineup_id` (XOR). Server and UI show a friendly error when the constraint is violated (e.g. "Complete the opponent lineup on the game page if you're placing an opponent shot").
- **New game form** – Step 4 (Opponent lineup) now includes helper text: "You can leave names blank; we'll use placeholder labels (e.g. Opponent Lead) that you can change later from the game page."

---

## [2.3.4] - 2026-03-11

### Changed

- **House diagram reference** – Centre line and Backboard text labels removed from the reference diagram; labeled reference shows Hog line, Back line, Tee line, and Hack line only. Diagram generation across the site (placement, replay) follows this template; the in-app diagram does not display Centre line or Backboard labels.
- **Default button color** – The default color for the **button** (center circle of the house) is now **white** (was green). Preferences and per-game custom colors unchanged; new users and games without custom colors will see a white button.

---

## [2.3.3] - 2026-03-11

### Added

- **House diagram: four ring colors** – Preferences and per-game override now include a separate **Button (center)** color in addition to 4-foot, 8-foot, and 12-foot. The diagram uses your chosen button color for the center circle.
- **Add/Edit shot positions opens with placement on** – The "Add/Edit shot positions" link on the game scoreboard now goes to the score page with `placement=1`, so the **Shot placement** toggle opens already On.
- **Dual-team scoring (Detailed mode)** – When **Track shot-by-shot scores for the opponent team** is enabled and the opponent lineup is set, the score page in **Detailed** mode shows all **16 shots per end** (standard) or **10** (doubles). Each shot is labeled **Our team** or **Opponent** and the correct curler name; saving uses `game_lineup_id` for our shots and `game_opponent_lineup_id` for opponent shots. Use **Detailed** mode to enter and edit opponent shots; Simple mode shows our team only with a note to use Detailed for opponent shots.
- **Opponent lineup: spaces in names** – When entering opponent curler names at game creation, spaces are preserved (e.g. "Jane Doe").
- **House diagram (WCF dimensions)** – Diagram now follows World Curling Rules 2025: sheet width 15 ft 7 in (sidelines at ±7.79 ft), view extends to backboard (y = -18); **hack line**, **backboard**, and **removed-stones zone** (between hack and backboard) with one-line help; **wheelchair lines** (dashed, 18 in from centre); **centre line** and **tee line** more visible; ring radii 6 ft / 4 ft / 2 ft / button 6 in (outcome_tag names 12ft/8ft/4ft kept for backward compatibility). **One-line in-UI help** for the removed-stones zone: "Stones removed from play (e.g. hogged, burned) appear here."
- **Rock-colored buttons** – Team-colored buttons (shot placement and Navigate) now use a **solid background** in the rock color with **contrasting text** (e.g. black on yellow, white on red) for readability in both light and dark themes.

### Changed

- **Placement: second click to move rock** – In placement mode you can click the diagram again to **reposition the delivered stone** without using "Clear all position."
- **Placement: curler attribution for 16-shot** – When tracking both teams, placement correctly attributes **our** vs **opponent** shots and saves with `game_lineup_id` or `game_opponent_lineup_id` accordingly.
- **Missing opponent lineup** – When track opponent scores is on but the opponent lineup is incomplete, a banner and link to the game page are shown; opponent shot slots show a clear message and CTA to complete the lineup.
- **Shot validation** – `createOrUpdateShot` accepts optional `game_opponent_lineup_id`; validation ensures exactly one of `game_lineup_id` or `game_opponent_lineup_id` is set per shot (not both).

---

## [2.3.2] - 2026-03-10

### Added

- **Playing area diagram (WCF proportions)** – View bounds extended so the full 12-foot ring and 4 ft behind the back line are visible (`DIAGRAM_VIEW_Y_MIN = -10`). House diagram now draws **sidelines** (sheet boundary), **button** (center circle with tooltip), and an **out-of-play strip** with 8+8 stones by team (when placement is on for standard). Placement sheet passes out-of-play stone counts and uses a square aspect container so the diagram is not cropped.
- **16 stones per end (standard, placement mode)** – When **Shot placement** is On and the game is **Standard**, the placement section shows **16 shot slots per end** (E1-S1…E1-S16). S1 = hammer lead, S2 = non-hammer lead, etc.; alternating by team. In-UI line: "16 stones = both teams (alternating)." Thrower labels on each button (curler name for us, "Opponent Lead/Second/Vice/Skip" for opponent). Backend and validation accept `shot_number` 1–16 for standard when saving placement (`createOrUpdateShot` with `maxShotNumber: 16`). Board state and replay support 16-shot ends; existing 8-shot games remain valid. Mobile: shot list is scrollable with touch-friendly targets.
- **Navigate card hidden when placement is on** – When you turn **Shot placement** On (Detailed mode), the **Navigate** card in the shot entry form is hidden so the placement panel is the single place to jump to (end, shot).
- **P shortcut and accessibility** – Tip text: "Focus a shot button (e.g. E1-S1) then press **P** to open the placement diagram." Each shot button has a **title** and **aria-label** describing the slot and "Open placement for this shot, or press P when focused."
- **Suggested goals: Accept, Modify, Decline, Delete** – For goals with status **Suggested** (from Virtual Coach), the Actions column now shows: **Accept** (set to active), **Modify** (open edit form: change target/deadline then save and set active), **Decline** (confirmation dialog then set to abandoned), **Delete** (confirmation dialog then permanently remove; suggested only). After each action the list refreshes; loading and error states are shown. New server action `deleteGoal(goalId)`; **CoachGoalForm** supports edit mode with `initialGoal` and "Save" button.

### Changed

- **Shot placement save** – Placement sheet now passes `maxShotNumber: 16` (standard) or `5` (doubles) when saving so shot_number 9–16 can be stored for standard games.
- **Board state** – `getMaxShotNumber(playFormat, placementBothTeams)` and `computeBoardStatesFromShots` support 16-shot standard (derived from data when any shot_number > 8). Validation and stone_ref allow shot_1..shot_16 for standard in that context.

---

## [2.3.1] - 2026-03-10

### Added

- **Virtual Coach Phase 6 (NLP and notes for coach)** – **Notes in context:** `buildCoachContext` appends recent game notes (last 5 team games with non-empty `game_notes`) and optional practice/drill summary (existing `practice_self_report_summary`); combined field `recent_notes_summary` with fixed limit 1000 chars (`COACH_CONTEXT_LIMITS.recent_notes_summary_max_chars`). **Semantic search:** Table `coach_note_embeddings` (team_id, game_id, note_text, embedding vector(1536)); RLS by team access; RPC `search_coach_note_embeddings(p_team_id, query_embedding, match_limit)`; `searchCoachNoteEmbeddings(query, teamId, limit)` and `embedGameNotesForCoach(gameId)` in `lib/actions/coach-notes.ts`; embeddings created/updated when game_notes are saved via `updateGame`. **Chat semantic notes:** When sending a coach chat message, semantic search runs in parallel with context build (when message length ≥ 12 chars and feature flag on); relevant note snippets (capped 500 chars, min similarity 0.5) are passed to `getChatReply` and included in the prompt. Timeout 5s; on timeout or error, chat proceeds without snippets. Feature flag `COACH_CHAT_SEMANTIC_NOTES_ENABLED` and constants in `lib/coaching/config.ts` control behavior; see `Documents/runbook-coach-semantic-notes.md`. **Shot position integration:** Context includes `position_summary` when Plan 2 data exists: shape `{ draws_4ft_pct, draws_8ft_pct, guards_guard_zone_pct, position_data_games_count, total_games }`; team uses `getPositionSummaryForTeam`, curler uses `getPositionSummaryForCurler` (shots via game_lineup). Goals: `target_metric` extended with `draws_4ft_pct`, `draws_8ft_pct`, `guards_guard_zone_pct`; `resolveCurrentValue` calls shot-position stats for these metrics (curler and team). Optional `curler_position` (lead/second/vice/skip) in context from `curlers.position`. LLM suggested-goals schema and `TARGET_METRICS` include position-based metrics. Coverage (position_data_games_count, total_games) available for coach to down-weight placement conclusions when sparse. Migration `20260310100013_coach_note_embeddings.sql`.
- **Doubles shot position: initial placement and power play side** – For **Doubles** games, the diagram and replay now show two pre-placed rocks at the start of each end (state_0): house rock (non-hammer) and guard (hammer), per WCF mixed doubles. New column `game_ends.power_play_placement_side` (`left` | `right` | null): when power play is used, you can set which side of the sheet the two pre-placed rocks were set so replay displays them correctly. Scoreboard end edit: when editing an end and marking **Power play**, a **Placement side: Left | Right** control appears (accessible label and description); optional hint when placement side is unset. When power play is used but placement side is not set, pre-placed rocks are shown in the center. Reset end and Delete end clear `power_play_placement_side`. Export/import and game CSV include the new column.
- **Shot Position Backlog (diagram, placement UX, score page layout, sync, team filter)** – **Diagram:** Playing end only, from 2 ft beyond the back line to just past the hog line (World Curling R1 dimensions). `lib/shot-position/zones.ts`: `HOG_LINE_Y = 21`, `BACK_LINE_Y = -6`, diagram view constants. House diagram uses zones for bounds; **Hog line**, **Back line**, and **Tee line** labeled. Replay uses same HouseDiagram and bounds. **Placement:** Click once to place delivered stone, click again to change position without clearing. **Score page (Detailed + Shot placement On):** Rubric card full width above (reduced font); form and Shot placement panel side-by-side (stack on narrow viewports). Simple/Both + placement: placement section below. **Sync:** When placement is On (Detailed or Both), current (end, shot) is shared: Navigate and Shot placement stay in sync. **Team filter:** Score page passes `track_opponent_scores`; when false, Navigate shows only our team's shots (by hammer per end); Shot placement always shows all shots per end. **Unified list and team colors:** When placement is On and slot is shared, E–S buttons are colored by team (our rock color / opponent rock color). **Revalidation:** `updateGame` revalidates score page so settings (e.g. track_opponent_scores) reflect after edit. **A11y:** Shot placement region and shot list have role/aria-label; helper text explains E1-S1 = End 1, Shot 1.
- **Shot Position Phase 4 (UX polish and stats integration)** – **House ring colors:** User preference `house_ring_colors` (already present) with defaults (green inner, white middle, light blue outer). New game form and Edit game settings form: optional "Use custom house ring colors for this game" with inner/middle/outer color pickers; stored in `games.house_ring_colors`. Diagram/replay resolve: game override → preference → defaults. **Placement UX:** Dedicated "Burned" control sets `burned: true` in position_outcome (stone placed out-of-play); soft plausibility warning when shot type and delivered zone don't match (e.g. Guard in 4-foot); optional tooltips "What is shot position?" and "Which stones can be moved?"; keyboard nudge (arrow keys) and Enter to confirm; first-time guidance mentions only stones on the board can be moved. **Zones (single source of truth):** `lib/shot-position/zones.ts` extended: guard zone (tee to hog excluding house), front of house, out-of-play (beyond hog/back line); `getOutcomeTagFromPosition(x,y)` for optional outcome_tag. **Stats and coach:** `lib/actions/shot-position-stats.ts` with `getPositionSummaryForTeam(teamId, profileId)`; **position_summary** shape for virtual coach Phase 6: `{ draws_4ft_pct, draws_8ft_pct, guards_guard_zone_pct, position_data_games_count, total_games, coverage }`. Optional outcome_tag derived on save from zone lookup in createOrUpdateShot. **RLS:** shots.position_outcome and games.house_ring_colors inherit existing table RLS; no new policies. **Tests:** Unit tests for computeBoardStatesFromShots (normal, variable-length, malformed, doubles), zones (guard zone, front of house, out-of-play, getOutcomeTagFromPosition), and position_outcome validation (bounds, stone_ref).
- **Shot Position Phase 3 (Replay view)** – New route `/games/[id]/replay` with same access control as score page (assertGameAccess). Replay page loads game and ends list; when user selects an end, that end’s shots (with position_outcome) for the resolved scorer are loaded via `getReplayDataForEnd` (lazy, one end at a time). Board state (state_0..state_N) is computed client-side with `computeBoardStatesFromShots`. House diagram in replay mode with lastThrownStoneRef and previousPositions (dot on last-thrown rock, outline circles for moved stones). UI: end selector; shot timeline (Previous/Next, slider); overlay shows "End X, Stone Y", curler name, "Stone X of this end", shot type, score. Accessibility: aria-labels, live region for current step. Optional "Play" animation (~1.2 s per shot) with `prefers-reduced-motion` respected (instant step when set). Cache: revalidatePath replay when position_outcome is saved. From game detail (scoreboard card): links to **Replay** and **Add/Edit shot positions** (score page).
- **Virtual Coach Phase 5 (Archiving and tracking)** – **Archiving:** Table `coach_archived_sessions` (session_id, entity_type, entity_id, profile_id, summarized_content, archived_at); archive job `runArchiveCoachSessions()` (sessions older than 90 days, batch 50; concatenate last 50 messages into summary, insert archive row, delete messages). Context builder includes `archived_summary` when present. Runbook `Documents/internal/runbook-coach-archiving.md` (retention: truncate on archive; batch limits). **Goal check-ins:** `listCheckInsForGoal`, `listCheckInsForGoals`, `addGoalCheckIn`, `getRecentCheckInSummaryForEntity`; Goals list shows recent check-ins per goal and "Check in" form; context includes `recent_check_in_summary`. **Practice/drill:** `coaching_drill_completions` extended with optional `difficulty`, `how_did_it_go`, `duration_min`; table `coach_self_reports` (entity, report_type drill|game, one_line); context includes `practice_self_report_summary`. **Game notes:** `games.game_notes` in edit form and Game setup card; `updateGame` accepts `game_notes`; context builder derives game situations (lost_after_leading, close_game, steals/forces/blanks, last_end_pressure) via `lib/coaching/derived-game-situations.ts` and `getDerivedGameSituationsForContext`; `derived_game_situations` in context. **Assessment/resource feedback:** Table `coach_assessment_feedback` (entity_type, entity_id, assessment_id or resource_id, rating, note); optional "Was this helpful?" (Yes/No) under narrative in Assessment view; `feedback_summary` in context. **Notifications and post-game CTA:** Table `coach_entity_visits` (profile_id, entity_type, entity_id, last_visited_at); `upsertCoachEntityVisit` on team/curler coach page load; coach landing shows "New assessment or goal" badge per entity when assessment `generated_at` or suggested goal `created_at` &gt; last_visited_at; post-game CTA on game detail when team or lineup curler has new content, linking to coach. Trigger and placement documented in `Documents/coach-notifications-cta.md`. Migration `20260310100011_coaching_v2_008_phase5_archiving_feedback.sql`.
- **Virtual Coach Phase 4 (Chat and sessions)** – Session lifecycle: create session when user starts coach or sends first message (`coach_sessions`: profile_id, entity_type, entity_id, started_at, trigger); end session on 30 min idle or explicit "End session" (set ended_at). **Timeout enforcement:** on-request only (no cron): on each message or get-session, if last activity is older than 30 min the session is ended and a new one is created if the user continues. Session actions: `getOrCreateSession`, `createSession`, `endSession`, `checkSessionTimeout`, `listSessionMessages` in `lib/actions/coach-session.ts`. Chat: each user message appended to `coach_messages`; context summary + last assessment summary + last 5 user/assistant pairs sent to large model; assistant reply persisted. Token control and rate limiting: `COACH_CHAT_LAST_K_MESSAGE_PAIRS` (5), `COACH_CHAT_RATE_LIMIT_PER_MINUTE` (10) in `lib/coaching/config.ts`; rate limit enforced in `sendCoachMessage`. Chat UI: `CoachChatView` with message history, input, example prompts ("Explain my top focus", "Give me one drill for this week", etc.), and "End session" button; integrated on team and curler coach pages. Context switching: `CoachEntitySwitcher` dropdown on team/curler pages; switching entity ends the current session (via `endOtherOpenSessions` in `getOrCreateSession`) and starts a new one for the new entity; eligibility re-checked per entity. Chat LLM in `lib/coaching/chat-llm.ts` (persona + context + last 5 pairs + latest user message). Router is not invoked in chat flow for Phase 4 (follow-up chat only).
- **Shot Position Phase 2 (House diagram and placement UI)** – House diagram component (`components/shot-position/house-diagram.tsx`): SVG from hog to back line + out-of-play space; 4'/8'/12' rings, tee/center line; ring colors resolved game override → user preference → defaults (`lib/shot-position/resolve-ring-colors.ts`). Coordinate transform screen ↔ feet; mode "place" | "replay"; stones, lastThrownStoneRef, previousPositions (outline circles); hit-test: click → (x,y); place mode delivered = one click, moved = select stone then click destination. Zone definitions in `lib/shot-position/zones.ts` (4ft, 8ft, 12ft, guard zone); suggested position: Draw → (0,0), Guard → guard zone point, Hit → none. Score page: "Shot placement" toggle shows placement section; Simple (delivered only) vs Full (delivered + moved) mode; "Place rocks" for every (end, shot) slot; placement sheet with diagram, Undo, Clear all, Use suggested, first-time guidance (localStorage dismissal). Save via createOrUpdateShot(position_outcome); position-only rows use sentinel shot type. Keyboard shortcut **P** when a shot slot button is focused opens placement. amendShotFromScorer does not copy position_outcome; post-hoc edits target current user's row.
- **Shot Position Phase 1 (Schema and state engine)** – Migration adds `shots.position_outcome` (jsonb, optional delivered + moved positions) and `games.house_ring_colors` (jsonb, optional inner/middle/outer hex for diagram/replay). Coordinate system: origin at button (0,0), feet, X horizontal, Y along sheet; validation ±20 ft, stone_ref shot_1..shot_8 (or shot_5 doubles) ≤ current shot_number. Pure client-safe `computeBoardStatesFromShots(shots, game, ends)` in `lib/shot-position/board-state.ts` (no server imports); returns state_0..state_N with previousPositions for moved stones. Server API `getBoardStateAfterShot(gameId, endNumber, shotNumber)` resolves scorer (primary_scorer_id → game creator → first scorer), loads shots with position_outcome via getShotsForGame, calls pure helper; returns stones { stone_ref, x, y, team }. Malformed position_outcome: skip that shot’s contribution and continue; replay data contract: client recomputes with same helper. Validation: `lib/validations/shot.ts` position_outcome schema (delivered, moved[], outcome_tag, burned); sentinel shot type "Position only" (category other, id constant) for placement-only rows; when payload has position_outcome but no real shot type, allow sentinel shot_type_id and score 0; exclude sentinel from stats. createOrUpdateShot accepts and persists position_outcome; getShotsForGame returns position_outcome.
- **Virtual Coach Phase 3 (LLM pipeline and narrative path)** – LLM provider abstraction in `lib/coaching/llm.ts` (provider-agnostic `complete(options)`; provider and model names from config: `COACH_LLM_ROUTER_MODEL`, `COACH_LLM_ASSESSMENT_MODEL`; extension reserved for tools/MCP). Routing: small model (e.g. gpt-4o-mini) takes structured context + optional last user message; output route (`needs_interview` | `ready_assessment` | `follow_up_chat`) and optional 1–2 sentence summary; parsed with Zod. Full assessment: large model (e.g. gpt-4o) with context + coach-persona.md and relevant focus-*.md; output developmental narrative (2–4 paragraphs), structured path (phases/milestones), optional suggested_goals; JSON mode and Zod validation; on parse failure retry once then fall back to rules-only; on LLM errors exponential backoff (max 2 retries, max delay 10s) then rules-only and message “Personalized narrative unavailable; showing focus areas and resources.” Narrative stored in `coach_assessments.narrative_text`, structure in `structure` jsonb, `summary_for_context` short; suggested goals inserted into `coach_goals` with status `suggested`. Prompt template in `content/coach/prompt-assessment.md`. Generate assessment: idempotency key support; `pg_advisory_lock` (migration `20260310100007_coaching_v2_007_advisory_lock.sql`) so only one assessment runs per entity; release after persist. Per-entity rate limit (max 6 per hour); context cache key helper `getContextCacheKey` (request-scoped; document Redis for multi-instance). Observability: log model, token count, request id. UI: “Generate personalized assessment” button on team and curler coach pages; loading state; narrative and milestones in Assessment view; fallback message when rules-only. Unit tests: router and assessment JSON parsing (Zod), context cache key.
- **Virtual Coach Phase 2 (Interview flow and context builder)** – Interview triggers: config thresholds for data thin (curler shots < 20, team games < 3), major improvement (e.g. gain ≥ 0.10 over last 5 vs previous 5 games), and stagnation (no gain over last 5). When user opens coach for an entity, triggers are computed; if onboarding (no prior answers), data thin, post_improvement, or stagnation, the corresponding question set is shown (from markdown). Answers stored in `coach_interview_answers` via server action `upsertInterviewAnswers`. Interview UI: gate component shows short “What is Virtual Coach?” and “Why we’re asking these questions” (dismissal persisted in localStorage); form loads question set by key and renders text, scale, and choice inputs; submit upserts answers and allows proceeding to assessment. Mobile-friendly layout and accessible controls (labels, focus). Structured context: `CoachContextSummary` in `lib/coaching/types.ts` with `COACH_CONTEXT_LIMITS` (interview_summary 500 chars, last_assessment_summary 800 chars); optional benchmarks and season_id. `buildCoachContext(entityType, entityId, teamId, options?)` in `lib/coaching/context.ts` calls stats RPCs, latest assessment, goals, interview answers; returns structured object for Phase 3 LLM. Content versioning documented for cache invalidation when markdown changes. Markdown: coach-persona (boundaries: no medical/mental-health advice; supplements in-person coach), interview-onboarding, interview-thin-data, interview-post-improvement, interview-stagnation, and focus-*.md (guard, draw, take-outs, weight-control, strategy, decision-making, getting-started). Empty-state copy when entity has no phases: “Record a game to unlock your assessment”.
- **Virtual Coach Phase 1 (Foundation)** – New Virtual Coach v2: schema (coach_team_eligibility, coach_curler_eligibility, coach_goals, coach_sessions, coach_messages, coach_assessments, coach_interview_answers, coach_goal_check_ins, games.game_notes), RLS on all new tables. Content loader for `content/coach/` markdown (coach-persona, interview-onboarding, interview-schema). Eligibility: `canUseCoachForEntity`, `getCoachEligibility`; admin Coaching access page extended with Team eligibility and Curler eligibility tabs (enable/disable coach per team/curler). Feature flag `VIRTUAL_COACH_ENABLED` (env); nav Coaching link points to `/coach` when enabled. Goals: server actions createGoal, updateGoal, listGoalsForEntity, resolveCurrentValue; goals list and add/edit UI with target_metric dropdown. Developmental path: on first coach open for entity, rule engine runs and one coach_assessments row is created (phases + focus_area_ids + resource_ids); Assessment view at `/coach` and `/coach/teams/[teamId]` and `/coach/teams/[teamId]/curlers/[curlerId]` shows phases and resources (no chat). Interview: schema in markdown, parser in `lib/coaching/interview-schema.ts`, trigger thresholds documented (no interview UI yet). Types in `lib/coaching/types.ts` (CoachContextSummary, assessment JSON, router types) as contract for Phase 2/3. No LLM or token usage.

---

## [2.2.22] - 2026-03-05

### Added

- **Clear filters** – On pages that use filters (team stats, public team stats, games list, shooting, hammer, thinking time, curler stats, season stats), a **Clear filters** link or button appears when any filter is active. It navigates to the same page with no filter query params so you can quickly include all data again. The date range filter also has a **Clear** button to remove only the date range while keeping period/format/type.

### Changed

- **Games nav icon** – The left-hand navigation icon for the Games page is now a custom curling rock icon (minimal outline: oval body, U-shaped handle, band line) instead of the gamepad icon. The icon uses `currentColor` so it adapts to light and dark theme.
- **Public team page filter parity** – The public team stats page (`/t/[slug]`) now matches the dashboard team stats page: **Game type** filter (All / Rec league / Competitive league / Bonspiel / etc.) is available when the period has two or more game types; filter options (play formats and game types) are derived from games in the selected period. The public **games list** (`/t/[slug]/games`) respects **play format** and **date range** (From/To) via URL params; **View games** from the public team page and **Back to team stats** from the games list preserve the current filters so the list and stats stay aligned.

---

## [2.2.21] - 2026-03-05

### Fixed

- **Team stats: Cumulative score progression** – The line chart now uses theme-aware colors (`--color-chart-1` / `--color-chart-2`) so the trend lines and data points are visible in both light and dark mode. Data points are always shown (not only on hover).
- **Team stats: Game situations summary** – Removed the stray "def" after the stolen-against percentage so the row matches the format of other percentage rows.

### Changed

- **Team stats: Layout** – Scoring by end number and Blank end rate cards are shown side by side; Game situations summary and Points per end distribution are shown side by side when space allows, to reduce vertical scrolling.
- **Team stats: Power play card** – The Power play usage and impact card is now shown only when the filtered period includes at least one doubles game (i.e. hidden when all games are Standard format).
- **Team stats: Points per end distribution** – Buckets are now 0, 1, 2, 3, and 4+ (previously 0, 1, 2, 3+).
- **Team stats: Shooting by category over time** – Replaced the table with a line chart (Guard %, Draw %, Hit % by game).

### Added

- **Team stats: Date range filter** – A date range filter (From / To) lets you restrict all stats to games played within the selected period. Applies to cumulative score, scoring by end, blank end rate, power play, comeback stats, points per end distribution, and first/second half comparison. As of this release the filter also applies to **RPC-based** stats: summary cards (games played, total shots, team shooting %, normalized avg), record (W–L, points for/against), game situations summary (hammer/steal/force), shooting-by-category-over-time chart, and curler list (via optional `p_date_from` / `p_date_to` on team-stats RPCs). **Export stats CSV** respects the selected date range when `date_from` / `date_to` are provided. **Public team stats** page (`/t/[slug]`) supports an optional date range (query params `date_from` / `date_to` or From/To inputs); all displayed stats and the games list are limited to that range when set.

---

## [2.2.20] - 2026-03-04

### Added

- **Delete curler on /curlers** – Identity owner can delete a person profile from the Curlers page (confirmation dialog). Game lineups are anonymized (curler_id set to null, display_name kept) and coaching drill completions unlinked so stats remain; then curlers, shares, invitations, and identity are removed. Team owner can delete an unlinked curler (synthetic row) when that curler has no game lineups.
- **Dashboard onboarding card** – A highlighted “Get started” card can appear on the dashboard with links to create a team, add curlers, create a game, score a game, and view stats. You can Dismiss it (saved in preferences); “Show get started tips” brings it back. Toggle in Preferences controls visibility for all users (with or without data).
- **Preferences page** – New Preferences link in the sidebar (Overview). Settings: Show get started / onboarding card on login (toggle), Theme (Light/Dark), Default shot scoring mode (Simple/Detailed/Both). Changes save automatically. Theme is synced with the header toggle and persists across devices. Migration `20260308000000_user_preferences` adds the user_preferences table.

---

## [2.2.19] - 2026-03-04

### Changed

- **Game scoring: Concession by team and color** – On the game scoreboard, concession is now shown by team name and rock color (e.g. "Jackson (Red) Conceded") instead of "We conceded" / "Opponent conceded". Button labels use the same format.

### Fixed

- **Share curler: already has access** – When sharing a curler with an existing user who already has access, the app now shows a clear message ("This user already has access to this curler.") instead of a raw database error.

### Added

- **Remove shared access** – In the Share curler dialog, a "People with access" section lists users who have been shared the curler, with a "Remove access" button to revoke access for each. Migration `20260307000000_curler_identity_shares_delete_team_owner` allows team owners to revoke shares for curlers on their teams.
- **Advanced team statistics** – On the team stats page, a new "Scoring & game situations" section includes: cumulative score progression (line chart of running points for/against), scoring by end number (average points per end 1–8 or 10), blank end rate, power play usage and impact, comeback wins and lost-after-leading counts, points-per-end distribution (0, 1, 2, 3+), game situations summary table (steals, forces, hammer ends from existing hammer stats), shooting by category over time (Guard/Draw/Hit % by game), and first half vs second half of period comparison.

---

## [2.2.18] - 2026-03-04

### Added

- **Share curler by email** – The Share dialog on the Curlers page now accepts an **email address** instead of a profile ID. If the email belongs to an existing user, the curler is shared immediately. If not, an **invite link** is generated that you can copy and send; when the recipient signs up or signs in with that email and opens the link, they accept and get access to the curler. New route: `/accept-curler-share?token=...` for accepting invite links. Migration `20260306000000_curler_identity_share_invitations` adds the pending-invitations table.

---

## [2.2.17] - 2026-03-04

### Fixed

- **Curlers page: Share button for all curlers** – Root cause: most curlers had no `curler_identity` record (they were "unlinked curlers"), so the sharing system had nothing to share. Migration `20260305100000_backfill_unlinked_curlers_identities` creates a `curler_identity` for every curler that was missing one and links them. The `createCurler` action now auto-creates an identity when none is provided, preventing recurrence.
- **Revoke share: team-owner fallback** – Team owners can now revoke shares for curler identities linked to curlers on their teams, consistent with the share action.

---

## [2.2.16] - 2026-03-04

### Changed

- **Curlers page: Share for curlers on teams you own** – The Share button now also appears for curlers linked from a team you own. Team owners see Share for all curlers on their teams even when `curler_identities.created_by` or `curlers.created_by` was not set or differs (e.g. after backfill only some identities had a creator).

---

## [2.2.15] - 2026-03-04

### Added

- **Migration: backfill curler_identities.created_by** – Migration `20260305000000_backfill_curler_identities_created_by.sql` sets `curler_identities.created_by` from the earliest linked curler (by `created_at`) where the identity had no creator. Run this migration so the Share button appears for all curlers you created; identities that already had a creator are unchanged.

### Fixed

- **Curlers page: hydration error on Create Curler form** – The Create Curler form uses `suppressHydrationWarning` to avoid hydration mismatches when browser extensions (e.g. LastPass) inject DOM into form fields. Hydration errors in dev from such extensions are no longer reported for this form.

---

## [2.2.14] - 2026-03-04

### Fixed

- **Curlers page: Share button for all curlers you created** – The Share button now appears for every curler you created or first linked, including those whose person profile has no creator recorded (legacy data). Sharing is allowed when you are the identity creator or when you added a curler that links to that identity and the identity has no other creator.

---

## [2.2.13] - 2026-03-04

### Changed

- **Curlers page layout** – The "Create a curler" card is now **above** the "Your curlers" card (stacked, full width). The Create a curler card uses a **compact layout** (smaller header, first/last name on one row, identifier and Add to teams inline with the Create button) to minimize vertical space.
- **Dev: webpack warning** – The webpack dev warning "Serializing big strings" from the cache strategy is suppressed in `next.config.ts` for a cleaner dev console.

---

## [2.2.12] - 2026-03-04

### Added

- **Curlers page: all curlers on teams you access** – The Curlers page now lists every curler you can see: identities you created or that were shared with you, plus curlers on teams you own, collaborate on, or score for. Unlinked curlers (no person profile) appear as separate rows; you can view stats or edit them from the team curlers page. "Add to team" and identity Edit/Share apply only to curlers with a person profile.

### Changed

- **Sharing: only creator can share** – Only the creator of a curler identity can share that curler with others. If someone shared a curler with you, you cannot share that same curler further (Share button is shown only when you created the identity).
- **Curler stats: Last N games table order** – The game list in the "Last N games" card is sorted with the most recent game first (date descending).
- **Chart export: Download as PNG** – Chart PNG export now uses **html-to-image** instead of html2canvas so Recharts (SVG) charts export correctly. "Download as PNG" on team shooting trend, category trend, shooting by end, shot-type distribution, season comparison, and hammer efficiency charts should now produce valid images.

---

## [2.2.11] - 2026-03-04

### Added

- **Curler stats page: conditional filters** – On a curler’s stats page, filters are shown only when relevant: **Team** when the curler is on more than one team; **Game format** and **Game type** only when the curler has data in two or more formats/types; **Season** (renamed from Period) only when they have stats in two or more seasons. Only options in which the curler has participated are listed. **Date range** filter (From/To) at the top of the filter section; applies to the by-game list and charts.
- **Stats page: View by Curler when “All” selected** – When you select **All** in the View by curler team filter, the app lists all curlers you have access to who have stats. Same person on multiple teams appears once; the link opens their curler stats page (combined identity stats when linked).
- **Add-curler form: card grouping** – The first name, last name, identifier, position, and Add curler button are wrapped in a card (same style as the existing-person section) to separate them from the “Add an Existing Curler” card.
- **Add an Existing Curler to the Team** – The optional person-linking section is renamed from “Optional: Same person on another team?” to **Add an Existing Curler to the Team**. A **picklist** of curlers on your other teams lets you choose an existing person to link (or add to this team) without searching by name.
- **Curlers page and sidebar** – New **Curlers** item in the sidebar links to `/curlers`. The Curlers page lists all curlers you have created or have access to (including those shared with you), sortable by last name, first name, or team count. You can **create a new curler** (optionally add them to one or more teams), **add a curler to a team**, **view stats**, **edit** name/identifier, and **share** a curler you created with another user (by profile ID). Shared users can add that curler to their own teams. New table `curler_identity_shares` and migration for sharing.

### Changed

- **Curler stats: “Period” renamed to “Season”** – The period filter on curler stats pages is now labeled **Season**.
- **Add-curler / edit curler: “name above” → “name below”** – Helper text and button now say “create a person profile from the name **below**” and “Create person profile from name **below**” so the copy matches the layout (the name fields are above the search/link section).

---

## [2.2.10] - 2026-03-04

### Added

- **color-scheme for light/dark theme** – `:root { color-scheme: light }` and `.dark { color-scheme: dark }` in `app/globals.css` so native form controls and widgets (inputs, scrollbars) render correctly in both themes.
- **Site font: Roboto** – Roboto is loaded via `next/font/google` and applied site-wide (layout + globals.css `font-sans`).

### Changed

- **Curler add/edit form: simplified copy and layout** – The optional person-linking section is now titled **Optional: Same person on another team?** with helper text: "Search by name to link for combined stats, or create a person profile from the name above." The button **Create as new person** is renamed to **Create person profile from name above**. This section is wrapped in a subtle card (border + bg-muted/30) in both the add-curler form and the curler edit row so the primary action (**Add curler** / **Save**) is clearly separate from the optional linking step.

---

## [2.2.9] - 2026-03-03

### Fixed

- **Hammer efficiency formula** – Hammer efficiency is now calculated as (ends with hammer where you scored 2+) / (ends with hammer where points were scored by either team). Blank ends are excluded from the denominator. This fix applies to the game stats page, team stats summary, public team stats, and the hammer drill-down page (per-game table and trendlines). Previously the denominator incorrectly used only ends where you scored at least 1.

---

## [2.2.8] - 2026-03-04

### Added

- **Game statistics card: With/Without Hammer layout** – The game page stats summary now shows four lines per card: Ends With/Without Hammer, Hammer Efficiency / Forced Ends rating (with fraction and %), Steal Defense / Steals rating (with fraction and %), and Total With/Without Hammer rating.
- **Team stats: conditional filters** – Game format and Game type filters on the team stats page only appear when the selected period has at least two options (e.g. Game format only when the team has both Standard and Doubles games; Game type only when the team has two or more game types). Only options that have games in the period are shown.

---

## [2.2.7] - 2026-03-03

### Fixed

- **Game statistics: substitute curler name** – On the game page, the Game statistics card now shows the curler's name (e.g. "Jenn Myers (TCC)") instead of the position ("Lead") when the lineup slot is linked to a curler but has no stored display name.

---

## [2.2.6] - 2026-03-03

### Fixed

- **Single-game curler stats: Guard/Draw/Hit cards show subcategories** – When viewing a curler’s stats for a specific game (e.g. from the game page or via the "Game stats" link), the Guard, Draw, and Hit cards now list each shot type (e.g. Hit & Roll, Come Around) with shot count and percentage for that game, instead of only showing the parent category total.

---

## [2.2.5] - 2026-03-03

### Added

- **Game-scoped curler stats** – From the game page, clicking a curler name in the Game statistics card opens that curler's stats for **that game only** (with a "This game" period pill). The Period filter lets you switch between "This game", Lifetime, and individual seasons. From the curler stats page, the "Last N games" Game list table now includes a **Game stats** link per row to view that curler's stats for that specific game. Works on both dashboard and public team pages.

### Security

- **Supabase Security Advisor** – RLS enabled on 10 public tables (team_collaborators, team_invitations, coaching_suggestion_feedback, coaching_drill_completions, coaching_access, coaching_suggestions, game_scorer_invitations, coaching_resources, coaching_resource_focus_areas, focus_areas); direct API access with anon key is denied; app behavior unchanged (service role bypasses RLS). Function search_path set on set_coaching_resource_embedding, search_coaching_resources, get_team_hammer_stats. Vector extension remains in public by design (documented); extensions schema created for future use. Permissive curler_identities_insert_authenticated policy dropped; inserts remain server-side only.

---

## [2.2.4] - 2026-02-23

### Changed

- **Project file organization** – Resolved `lib/utils` naming clash: `cn` moved to `lib/utils/cn.ts`, with `lib/utils/index.ts` re-exporting cn, hammer, curler, and game utilities. Game rule `getNextHammer` moved from validations to `lib/utils/game.ts` (re-exported from validations for backward compatibility). Deduplicated `secondsFromPair`: now exported from `lib/thinking-time.ts` and used by stats actions. Game-related components (game-scoreboard, game-scorers-section, compare-scorers-table, score-game-link, lineup-editor, opponent-lineup-editor) moved under `components/games/`. Markdown guide UI moved from `lib/docs/markdown-guide.tsx` to `components/docs/markdown-guide.tsx`. Documentation (test scripts, UAT spec, README, administrator guide) version references updated to 2.2.3; README and Changelog updated for refactoring and file-organization changes.

---

## [2.2.3] - 2026-02-21

### Added

- **Link to same person: search finds curlers on your other teams** – When adding a curler, "Link to same person" search now finds curlers on your other teams even if they are not yet linked to an identity. Selecting an unlinked curler creates and links an identity automatically so both teams share stats for that person.
- **Add-curler form: Search button and Enter key** – The "Link to same person" search can be run with an explicit **Search** button or by pressing **Enter** in the search field (in addition to blur).

### Fixed

- **Add-curler link search returned no one** – Search now includes curlers on accessible teams (by name), not only existing curler_identities, so you can link to someone on another team without having linked them there first.

---

## [2.2.2] - 2026-02-21

### Added

- **Curler identity search scoped to accessible teams** – "Link to same person" now returns only identities linked from curlers on teams you can access (owned, collaborated, or games you score). You can link the same person across your teams and teams you're invited to (e.g. same curler on two teams when one owner invited the other).

### Documentation

- **End-user guide** – Added "Linking a curler to the same person (for stats)" and same-person-across-teams (including teams you're invited to). **README** and **Administrator guide** updated for identity search scope and cross-team linking. **Test scripts** and **UAT spec** updated with cross-team linking case (C-P9, TC-030 CURLER-09).

---

## [2.2.1] - 2026-02-20

### Fixed

- **Per-game hammer breakdown** – Ends with hammer after a blank end were undercounted on the With hammer / Without hammer drill-down page. Hammer is now derived from the game's first-end hammer and the ordered sequence of end scores so blank ends keep hammer correctly.
- **Season-level hammer stats** – The `get_team_hammer_stats` RPC now derives hammer from each game's first-end hammer and ordered end scores instead of stored `hammer_team`, so team stats summary, public page, coaching, and export show correct season totals.

### Changed

- **Team shooting trend page** – The duplicate "By specific shot type" bar chart (same as "Shooting by category") is replaced with an "In-turn vs out-turn" card showing shooting percentage with last-stone advantage vs without for the selected period.

---

## [2.2.0] - 2026-02-20

### Added

- **Coaching Phase 3** – **Semantic search:** Search drills and resources by natural language on the Coaching hub (Search drills & resources). Uses pgvector and OpenAI embeddings; results open via external links. **User-submitted resources:** "Suggest a resource" form (URL, title, optional note); submissions are stored as pending until an admin approves and tags them with focus areas. **Admin → Coaching submissions:** List pending user submissions; Approve (with optional focus areas) or Reject (deletes the submission). Approved resources appear in the suggestion pool and in semantic search. **More curated resources:** Curling Class and APITG (Glenn Paulley) links added by category (drills, theory, strategy; articles/podcasts on strategy, decision-making, delivery). **Suggestion ranking:** Suggested resources are now ordered by completion count and thumbs-up rate (from feedback), then by difficulty, so higher-performing and better-rated resources appear first. Schema: pgvector extension, `coaching_resources.embedding` (vector 1536), ivfflat index, and RPC `search_coaching_resources` for similarity search. See User Manual and Administrator Guide for usage and backfill notes.

---

## [2.1.0] - 2026-02-20

### Added

- **Coaching Phase 2** – **Handbook drill reference:** Optional seed adds Curling Drills Handbook (Curl Coach) drill metadata and links to the PDF; no full handbook text stored. **Smarter rules:** Trend-based (last 5 games vs previous 5): if recent performance drops by 10+ percentage points, the engine adds or emphasizes weight control and records rule_id `curler_trend_declining`. Position-aware: if a curler is much weaker in one position, adds focus and rule_id `curler_position_weak`. **Optional AI narrative:** When enabled (`COACHING_AI_NARRATIVE_ENABLED` and `OPENAI_API_KEY`), suggestions can include a short 2–3 sentence AI-generated summary (stored in `coaching_suggestions.narrative_text`). **Done N times:** When marking a drill complete you can enter how many times (default 1); the curler coaching page shows "Completed N times" per resource (sum of times_count, scoped by curler/team when provided). Schema: `coaching_resources.content_for_search` (nullable, for future semantic search); `coaching_suggestions.narrative_text` (nullable).

---

## [2.0.1] - 2026-02-20

### Changed

- **App and file structure optimization** – Dashboard layout now runs auth, coaching access, profile upsert, and app version in parallel so time-to-layout is reduced. Team stats and shooting pages load chart-heavy UI in a separate JS chunk via `next/dynamic` (smaller initial bundle). Game score page loads the scoring view dynamically. Route-level loading boundaries (skeletons) were added for dashboard, team stats shooting, game detail, and game score so users see a loading state instead of a blank screen. **ARCHITECTURE.md** (project root) documents conventions: no barrel exports for components or actions, feature-based component folders, server components by default, heavy client UI loaded via `next/dynamic`. **next.config:** `experimental.optimizePackageImports` enabled for `lucide-react` and `recharts` to trim unused exports.

---

## [2.0.0] - 2026-02-19

### Added

- **Coaching (MVP)** – Gated **Coaching** section in the sidebar (visible only to admins and users granted Coaching access). Rule-based focus areas and suggested drills/resources for curlers and teams, using existing stats (guard, draw, hit, weight control, hammer, steal/force, thinking time). Scottish Curling drills and curated links (Try Curling, World Curling Federation, Scottish Curling resources) are seeded; suggestions are generated by a rule engine (no AI). Users can open suggested resources via external links, **mark a drill done** (completion recorded once), and give **thumbs up/down** on suggestions. Rolled out to admins and invited users via **Admin → Coaching access** (Grant/Revoke). Minimum games/shots apply for personalized suggestions; otherwise “Getting started” and generic resources are suggested.

---

## [1.6.13] - 2026-02-19

### Fixed

- **Team invite by link (new users)** – The accept-team-invite page copy now tells users without an account to sign up with the invited email first (they return to the page to accept), and users with an account to sign in. New users who sign up from the invite link can now accept the invitation immediately after sign-up; the app creates their profile on accept when needed, so they no longer see "You must be signed in to accept" or need to visit the Teams page first.
- **Club scoreboard inline entry** – Club scoreboard cells that showed "—" are now inline editable when you can edit ends: click a "—" cell to enter end numbers (e.g. `3` or `2 4 7`). Entering end numbers in a point-total column (1–15) sets that end’s score so the cumulative matches the column; column 16 sets those ends to 0–0 (blank). The app ensures enough ends exist and updates scores accordingly.
- **Blank ends in column 16 (both rows)** – Blank ends (0–0) now appear in column 16 for both teams’ rows on the club scoreboard, not only the first team’s row.

---

## [1.6.12] - 2026-02-18

### Fixed

- **Game statistics use official scorer only** – The curlers/stats table and pie chart at the bottom of the game page now use only the scorer selected in **Use for official stats** (primary scorer). When no primary scorer is set (e.g. "All scorers"), all shots are counted as before.

### Added

- **Club scoreboard editable inline** – On the game page, when you can edit ends, each end number in the club scoreboard (point-total columns 1–16) is clickable: clicking an end number scrolls to that end’s row in the End scores section and opens inline edit for that score. The End score section remains editable inline as before (click a score cell to edit).

---

## [1.6.11] - 2026-02-18

### Fixed

- **View scoring by: "(no longer has access)"** – The label now appears only for scorers who truly no longer have access. Team owners and team collaborators who have scored are no longer incorrectly marked as "(no longer has access)" when they still have access via the team.

### Added

- **Compare scorers: who can amend** – Team collaborators (and game/team owners) can use **Use [Scorer A]** / **Use [Scorer B]** on the compare page to copy another scorer’s shot into their own scoring; previously only users listed in game_scorers or the game owner could amend.
- **Compare scorers: row highlighting** – Rows where both scorers agree (same type and score) are highlighted in green; rows where they differ are highlighted in red.

---

## [1.6.10] - 2026-02-18

### Added

- **Default primary scorer** – When you create a game, your scoring is now used for official stats by default (primary scorer is set to the game creator). You can still change or clear the primary scorer from the game page.
- **Scorers section: view scoring by each scorer** – The Scorers section on the game page now lists everyone who has scored the game under **View scoring by**. Each name links to a shot scoring summary for that scorer (`/games/[id]/summary?scorer=...`). Scorers who no longer have access to the game are marked **(no longer has access)**; their scoring remains viewable.
- **Compare scorers when 2+ have scored** – The **Compare scorers** link appears when at least two people have entered shots (even if one was later removed from the game). The compare page uses **scorers who have scored** for the dropdowns and for the "need 2" check.
- **Timestamps and YouTube links in compare** – On the compare page, when the game has thinking time and a YouTube stream URL, a **Timestamps and YouTube links (by end)** block lists each end’s time ranges with clickable links to jump to that moment on YouTube.
- **Amend your score from another scorer** – On the compare page, when you can score (you are a scorer or game owner), each shot row has **Use [Scorer A]** / **Use [Scorer B]** buttons to copy that scorer’s type and score for that shot into your own scoring.
- **Delete another user’s game scoring** – The team or game owner can **Delete their scoring** for any other scorer who has scored the game (with confirmation). If that scorer was the primary scorer, the game’s primary scorer is cleared so stats no longer point at empty data.
- **Primary scorer from “who has scored”** – The **Use for official stats** dropdown lists everyone who has scored the game (including users no longer in the scorers list), so you can keep or set primary to a removed user’s data. Setting primary is allowed for any profile that has at least one shot for the game.

### Unchanged

- **Scoring persists when access is removed** – Removing a user from the game or team does not delete their shots; their scoring stays attached to the game and remains visible in View scoring by and in Compare.

---

## [1.6.9] - 2026-02-18

### Fixed

- **Game statistics pie chart tooltip** – The pie chart (shots by category) now shows the shot type (Guard, Draw, Hit) in the hover tooltip label instead of "Shots" for every segment.

### Added

- **Game statistics card: thinking time in expandable row** – When a game has thinking time tracked, the expandable row per curler in the Game statistics card now shows a **thinking-time summary** for that curler (total seconds and average seconds per rock).
- **Shot scoring summary: Notes column** – The shot scoring summary table (`/games/[id]/summary`) now includes a **Notes** column when scorers have entered comments; the column is shown only when at least one shot has notes.
- **Team detail: Games link** – The team detail page now has a **Games** link (same style as Curlers, Statistics, Seasons) that opens the Games list filtered to that team (`/games?team=...`).

---

## [1.6.8] - 2026-02-18

### Added

- **Game statistics card (with shots)** – When a game has shots scored, the Game statistics card now includes: a **pie chart** of the percent of shots by category (Guard, Draw, Hit); **With hammer** and **Without hammer** stats for the game (same metrics as on team stats); per-curler columns **In-turn %**, **Out-turn %**, **Guard %**, **Draw %**, **Hit %**; each curler’s name **links to that curler’s stats page** when the lineup slot is linked to a curler; an **expandable row** per curler with in-game breakdown by turn and category.
- **Curler stats: by shot type over time** – On the curler detail page (`/teams/[id]/stats/curlers/[curlerId]` and public `/t/[slug]/curlers/[curlerId]`), the **By shot type** section now includes **trendline graphs** of shooting percentage by category (Guard %, Draw %, Hit %) per game over time.
- **Curler stats: last games table** – Under the “Last N games” card on the curler detail page, a **table** lists each game with Date, Opponent, Shots, Shooting %, and a **link to the game** (dashboard: `/games/[id]`; public: `/t/[slug]/games/[id]`).

---

## [1.6.7] - 2026-02-18

### Changed

- **Testing game thinking time** – When admins create a testing game, thinking time pairs now include `start_seconds` and `stop_seconds`. The data is stored in `game_ends.thinking_time_pairs` and appears in team stats (Thinking time by position), curler thinking-time summaries, and exports.

---

## [1.6.6] - 2026-02-18

### Changed

- **Thinking time stats without YouTube** – Thinking time now counts in team and curler stats whenever start/stop are entered, even when the game has no YouTube stream URL. The app always computes and stores `start_seconds`/`stop_seconds` when start/stop are present, so thinking-time aggregation works with or without a YouTube link.
- **Code review and optimization** – Games list now uses a shared data layer: the main Games page (`/games`) calls `getGamesList` from `lib/actions/games.ts` and uses the shared `GameListItem` type instead of duplicating query and enrichment logic. Removed duplicate `GameRow` type. Thinking-time type consolidated: the testing game generator imports `ThinkingTimePair` from `lib/validations/shot` instead of defining a duplicate. Public stats type exports standardized: `CurlerStatsByPositionItem` is re-exported from `lib/actions/public-stats` (the previous `CurlerStatsByPositionRow` alias was removed). Design options page (`/dashboard/design-options`) documented as a design reference not linked from nav. No change to UI/UX or behavior.

---

## [1.6.5] - 2026-02-18

### Added

- **Admin: Create testing game** – Admins can generate a full QA testing game from **Admin → Create testing game**. Select a user (profile), a team the user can access, and an optional season. The app creates an 8-end standard game (or 9 with tiebreaker if the randomized score is a tie) with: randomized opponent name (common English last names), date within the season range (or last 90 days if no season), randomized end scores (one team per end, max 3 points, blanks allowed; highest final score is kept under 10), game type "Rec league game", random rock colors and hammer, thinking time for all 15 pairs per end (Lead 20±2s, Second 23±2s, Vice 28±5s, Skip 60±10s), and shot-by-shot data (0–4 scale, position-based guard/draw/hit distribution). The game is created under the selected team and owned by the selected user; primary scorer is set so stats and export use this data.
- **Shots by type pie chart** – On the **team statistics** page (`/teams/[id]/stats`) and on each **curler stats** page (`/teams/[id]/stats/curlers/[curlerId]`), a pie chart shows the percentage of shots in each category (Guard, Draw, Hit). On the team page it uses overall team shot counts; on the curler page it uses that curler’s shot counts. The curler page also keeps the existing bar chart (average score by category) and detail cards.
- **Games: filter by team** – On the **Games** page (`/games`), a **Team** dropdown lets you filter the list to a single team. Choose "All teams" to show games from all your teams, or pick a team to see only that team’s games. The filter uses the URL (e.g. `?team=...`) and works together with the existing date, result, opponent, and Has stats filters.

---

## [1.6.4] - 2026-02-18

### Added

- **Score game prompt for team-invited users** – When a user who was invited to the team (not the team owner or game creator) clicks **Score game** on a game that already has scoring (shots or scoreboard), a dialog asks whether to **view the existing shot scoring summary** or **do their own scoring** for the same game. Choosing the summary opens the read-only summary page; choosing their own scoring opens the score page as before.
- **View shot scoring summary on Games list** – On the **Games** page (`/games`), each game now has two links: **View game** (game detail) and **View shot scoring summary**. The summary page (`/games/[id]/summary`) shows the **club scoreboard** and a **read-only table** of all scored shots (primary scorer when set, otherwise all shots). No editing on the summary page.

---

## [1.6.3] - 2026-02-18

### Added

- **Admin Settings: App URL** – Admins can set the **App URL** in **Admin → Settings** so invite links (team, game scorer, season) are generated correctly in production. The value saved in the database overrides `NEXT_PUBLIC_APP_URL` when both are set, so the URL can be changed without redeploying.

---

## [1.6.2] - 2026-02-18

### Changed

- **Invite links** – The app base URL for building invite links (team, game scorer, season) now prefers the `NEXT_PUBLIC_APP_URL` environment variable when set, so invite links appear reliably in production. Add `NEXT_PUBLIC_APP_URL` to your environment (e.g. `https://yourapp.com`). If it is not set and the server cannot determine the host, the UI shows a message that the invitation was created but a shareable link requires setting `NEXT_PUBLIC_APP_URL`.
- **Invite sections: behavior and no-email copy** – The **Team members**, **Scorers** (game), and **Collaborators** (season) cards now include short “How it works” text: if the invitee has an account they are added; if not, you get a link to copy and send yourself—the app does **not** send any email. The link text when a link is shown also states that you must copy and send it yourself (e.g. by email or messaging).

---

## [1.6.1] - 2026-02-18

### Added

- **Stats: game type filter** – Team and curler stats pages now support a **Game type** filter (All, Ad-hoc game, Rec league game, Competitive league game, Bonspiel, League other). Selection is stored in the URL and applied to all stats and export links.
- **Stats: View by curler** – On the main **Stats** page (`/stats`), a new **View by curler** section lets you select a team and see that team’s curlers with links to their individual stats (`/teams/[id]/stats/curlers/[curlerId]`).
- **Game setup summary** – The game detail page now shows a **Game setup** card below the scoreboard listing our team, opponent, date, season, game type, number of ends, play format, rock colors, hammer first end (and method for doubles), location, sheet, YouTube stream, and tracking options.

### Changed

- **Team invite** – Copy button and invite link display fixed: the full invite URL is visible and selectable (no truncation), and the Copy button has a reliable touch target. Documented that the app does **not** send invite emails—the inviter must copy and share the link manually (e.g. by email or messaging).
- **Team page layout** – Team detail page now uses a responsive grid: Settings and Team members cards sit side-by-side on medium+ screens; Curlers/Statistics/Seasons card spans full width below.
- **Game edit** – All game setup fields are now editable after creation: date, our/opponent names, game type, number of ends, track thinking time, track opponent scores, rock colors, hammer first end, hammer first end method (doubles), location, sheet, and YouTube stream URL. The **Edit game** form on the game detail page includes all of these.

---

## [1.6.0] - 2026-02-17

### Added

- **Phase B category trend** – Shooting drill-down now shows **Guard %, Draw %, Hit %** as three trend lines over games (date-scaled). New RPC `get_team_stats_by_game_and_category` and server action `getTeamStatsByGameAndCategory`; chart component `CategoryTrendLineChart`.
- **Shooting % by end** – New RPC `get_team_shooting_by_end` and chart **Shooting % by end** (ends 1–8) on the shooting drill-down to spot late-game drops or strong finishes.
- **Chart export** – **Download as PNG** on key charts: team shooting trend, category trend, shooting by end, shot-type distribution, season comparison, and hammer efficiency trend. Uses `html2canvas`; optional `ChartCardWithExport` and `ChartExportButton` components.
- **Public page parity** – Public team stats page now shows **category trend** and **Shooting % by end** with the same season and game-format filters. New public actions `getPublicTeamStatsByGameAndCategory` and `getPublicTeamShootingByEnd`.
- **Season comparison** – **Compare to previous season** on the shooting drill-down and from the main team stats page. When a season is selected, you can view shooting % for this season vs the previous season on one chart. Link and URL param `compare_to=previous`.
- **Shot-type distribution over time** – Chart showing how Guard/Draw/Hit **usage** (as % of shots per game) changes game by game on the shooting drill-down. Uses existing `get_team_stats_by_game_and_category` data.
- **Games list filters** – On **Games** (dashboard) and **Team stats → Games**: filter by **date range** (from/to), **result** (W/L/T), **opponent** (text), and **Has stats** (yes/no). Filters use URL search params. `getGamesForTeam` now returns `season_id` and `has_stats` and accepts optional `GamesListFilters`.
- **Inline score edit** – On the scoreboard end-scores table, you can **click a score cell** to edit that value in place (no need to open the full row Edit). Blur or Enter saves; validation and full row Edit unchanged.
- **Empty states and onboarding** – Consistent empty-state copy: **Add games and score shots to see stats** with link to New game. **Team onboarding card** on the team detail page when the team has no games: dismissible “Next steps: Create a game → Add ends → Score shots” with link to New game (dismissal stored in localStorage).

### Changed

- Team stats dashboard: **Team shooting % trend** card uses `TeamShootingTrendCard` with Download as PNG; link to **Compare to previous season** when a season is selected.
- Hammer drill-down: **Efficiency trendlines** card wrapped with `ChartCardWithExport` for PNG download.

---

## [1.5.1] - 2026-02-17

### Changed

- **Invite scorers: team owner, game creator, and admins** – The game owner (team owner), game creator, or an admin can now invite scorers, remove scorers, and set the primary scorer for a game. Previously only the team owner saw the "Invite scorer by email" form and the "Use for official stats" / "Remove" controls. Team collaborators who create a game now see these controls, and admins can manage scorers on any game they can access.

---

## [1.4.15] - 2026-02-17

### Fixed

- **Delete end / Reset end for all ends** – Delete end and Reset end are now available for **any end** (including ends 6, 7, 8) for everyone who can edit the game (team owner and team collaborators). Previously only the team owner saw these buttons, so collaborators could not remove or reset ends. The end-scores table **actions column** (Edit, Reset end, Delete end) is now **sticky on the right** when the table scrolls horizontally, so the buttons stay visible for every row on smaller viewports.

---

## [1.4.14] - 2026-02-17

### Added

- **Team shooting by shot type on drill-down** – The shooting drill-down page (`/teams/[id]/stats/shooting`) now shows **team-aggregated shots by specific shot type** in addition to the Guard/Draw/Hit category chart. When the team has shot data for the selected period, a **By specific shot type** section displays: a bar chart of average (0–100) by category (Guard, Draw, Hit), plus one card per category listing detailed shot types and percentages (e.g. Freeze, Takeout, Center guard). The same breakdown remains available on each curler’s stats page. New RPC `get_team_shooting_by_shot_type` and server action `getTeamShootingByShotType` in `lib/actions/stats.ts`.

---

## [1.4.13] - 2026-02-17

### Fixed

- **Shot-by-shot grid column shading** – In **Simple** and **Both** scoring modes, the shot grid now uses **four distinct background tints** (one per curler: lead, second, vice, skip) so all columns are easy to tell apart. Previously only the vice’s columns stood out in dark theme because the other positions used similar muted shades. The grid now uses the same chart color palette (chart-1 … chart-4) at low opacity for cell backgrounds and a matching left border between curler blocks. The fix applies in both light and dark theme and improves scannability on the dashboard and public score pages.

---

## [1.4.12] - 2026-02-17

### Fixed

- **Team stats record cards** – The record section on the team statistics page no longer showed duplicate **Losses** and **Points for** cards. It now shows exactly four cards: Wins, Losses, Points for, Points against.

### Added

- **Team shooting drill-down** – The **Team shooting % trend** card on the team stats page is now **clickable** and opens a new **shooting drill-down** page at `/teams/[id]/stats/shooting`. The drill-down shows: a **summary table** of shooting by game (game link, date, opponent, shots count, avg 0–100); the **overall team shooting % trend** chart with trendline; **shooting by category** (Guard, Draw, Hit) as a period-aggregate bar chart; and a note that per-shot-type breakdown is available on each curler’s stats page. Period and game format filters (Lifetime / season, All / Standard / Doubles) match the main stats page.

### Changed

- **Team shooting % trend card** – The card now links to the shooting drill-down and displays “View shooting drill-down →” for discoverability.

---

## [1.4.11] - 2026-02-17

### Changed

- **Efficiency trendlines (hammer page)** – Efficiency trendlines on the hammer stats page (With hammer / Without hammer) now use a **date-scaled x-axis**: points are spaced by actual game date, and trendlines are computed over time. Matches the behavior of the team shooting % trend chart.
- **Public team stats page** – The public team stats page (`/t/{slug}`) is aligned with the dashboard team stats page: **game format** selector (All / Standard / Doubles), **team shooting summary** (by position and by turn/category), **team shooting % trend** chart, **overall team efficiency** card, and **With hammer / Without hammer** cards with the same descriptions and layout. The curler list respects the selected game format.

---

## [1.4.10] - 2026-02-17

### Added

- **Copy from existing rubric** – When creating a new rubric at **Admin → Rubrics**, you can optionally **copy content from an existing rubric**. The new rubric’s per-shot-type description and scoring definition are prepopulated from the chosen rubric (e.g. copy from the 0–4 rubric when creating a 0–5 rubric), so you can adapt the text for the new scale.

### Changed

- **Team shooting % trend (and per-curler last N games) chart** – The trend line chart now uses a **date-scaled x-axis** when game dates are available: horizontal spacing reflects actual time between games (e.g. a month gap appears wider than a week), and the trendline is computed against time. If no game dates are present, the chart falls back to even spacing by game order.
- **One rubric per score scale** – At most one rubric may exist for each scoring range (0–4, 0–5, 0–6). The database enforces this with a unique constraint. The create-rubric form disables scale options that already have a rubric and shows a clear error if creation or edit would duplicate a scale.
- **Score page layout** – When a rubric exists for the game's scale, the shot form and rubric card are side-by-side (each 50% width) at viewports ≥768px (non-mobile) and stack on smaller viewports. When no rubric exists, the shot form uses full width at all breakpoints.

---

## [1.4.9] - 2026-02-17

### Added

- **Admin scoring rubrics** – Admins can create and edit scoring rubrics at **Admin → Rubrics**. Each rubric has a **name** and **score scale** (0–4, 0–5, or 0–6). For each shot type, admins enter a **description** and **scoring definition** (e.g. "4 Shot made as called. 3 Target removed… 0 Complete miss."). Entries are editable so content can be updated later.
- **Rubric on score page** – In **Detailed** and **Both** scoring modes, when the game has a rubric for its scale, a **rubric card** appears **side-by-side** with the shot entry form. The card shows the **description** and **scoring** text for the currently selected shot type. If the user selects a generic type (Hit, Draw, Guard), the card shows the first specific type in that category (e.g. Hit & Stick for Hit).
- **Keyboard shortcuts H / D / G** – In Detailed and Both mode, **H** sets shot type to first Hit, **D** to first Draw, **G** to first Guard. Shortcuts do not run while focus is in the notes field.

### Changed

- **Curl Coach hardcoded card removed** – The previous "Curl Coach shot scoring" / curling-stats-guide rubric card is replaced by the admin-defined rubric card. When no rubric exists for the game's scale (e.g. 0–6 with no rubric configured), no rubric card is shown.
- **Score page layout** – In Detailed/Both, the shot entry form and rubric card are side-by-side (each 50% width) on large screens and stack on narrow screens.

---

## [1.4.8] - 2026-02-17

### Added

- **Team shooting summary** – On the team stats page, a **Team shooting summary** card shows shot percentages **by position** (Lead, Second, Vice, Skip) and **by turn and category** (In-turn / Out-turn with Guard, Draw, Hit). Data from new RPCs `get_team_shooting_by_position` and `get_team_shooting_by_turn_and_category`.
- **Team shooting % trendline** – A **Team shooting % trend** card shows a line chart of overall team shooting percentage (normalized 0–100) per game with a trendline when there are at least two games. Data from new RPC `get_team_stats_by_game`.
- **Overall team efficiency (hammer)** – The hammer section on the team stats page now includes an **Overall team efficiency** card (average of hammer efficiency, steal defense, force efficiency, and steal efficiency). The hammer drill-down page shows the same overall efficiency and **efficiency trendlines**: two charts (ratings with/without hammer and overall; and the four component metrics) with per-game series and trendlines.
- **Curler By shot type: category chart and detail cards** – On per-curler stats, **By shot type** is restructured: the first chart shows only **Guard**, **Draw**, and **Hit** (sub-types rolled up to category). Below it, one card per category lists the detailed shot types and percentages (e.g. Center guard, Free guard) within that category. The same layout applies on the public curler page when shot-type data is available.

### Changed

- **Hammer drill-down** – Now shows overall team efficiency and trendline charts for each measure and for rating with/without hammer and overall efficiency.

---

## [1.4.7] - 2026-02-17

### Added

- **Mobile-friendly responsive layout** – The dashboard is now responsive. On viewports under 768px (phones and small tablets), the sidebar is hidden and a **menu icon** (hamburger) in the header opens a **drawer** (Sheet) with the same navigation (Dashboard, Teams, Games, Seasons, Stats, New game, User Manual, Admin if applicable). Main content uses full width on small screens. The drawer closes when you navigate to a page or tap outside.

### Changed

- **Dashboard layout** – Uses viewport breakpoints (768px) and a client chrome component: fixed sidebar on desktop, collapsible Sheet on mobile. Main content padding is responsive (`p-4 md:p-6 lg:p-8`). Header includes a menu button (mobile only) with a 44px touch target.
- **Touch targets** – Sidebar nav links and buttons have a minimum height of 44px for easier tapping on phones and tablets. Simple shot grid score cells use a 44px minimum tap target on small viewports.

---

## [1.4.6] - 2026-02-16

### Added

- **Reset end** – On the scoreboard, when editing any end (or from the row), the game owner can click **Reset end** to clear that end’s scores and all shot data for that end. Available even when the end has a score; confirmation required.

### Changed

- **Scoreboard: Delete end** – **Delete end** is now available whenever the game has at least one end (including when there is only one end). Deleting the only end leaves the game with 0 ends; use **Add end** to add ends again.
- **New games: 8 ends by default** – When you create a game, the scoreboard is pre-filled with 8 ends (or the **Number of ends** you chose in game setup) instead of 1. You can enter scores and add more ends up to 11 as before.

---

## [1.4.5] - 2026-02-16

### Added

- **Simple shot entry: "X" for throw-away / not thrown** – In **Simple** (and **Both**) scoring mode you can enter **X** in any cell for a shot to mark it as thrown away or not thrown. The shot is stored using an Other-category shot type (e.g. Throw Away, Not Thrown) and is **excluded from statistics** and from the per-end shooting percentage, matching the behavior of Detailed mode when you choose those shot types.

### Changed

- **Thinking time: fewer pairs in final end** – You may now enter **1 to 15** lines per end (standard) or **1 to 9** (doubles) instead of exactly 15 or 9. In the final end, if the last stone(s) were not thrown (e.g. skip with hammer doesn't throw their final stone), enter only the pairs you have; missing pairs do not count against aggregated thinking time. Ends with any thinking time data (1 or more pairs) now appear in the **Thinking time by team and position** summary on the score page.

---

## [1.4.4] - 2026-02-16

### Added

- **Breadcrumb: game and curler names** – The dashboard breadcrumb now shows the **game name** (e.g. "Our Team vs Opponent") instead of the game ID when viewing a game or its score page, and the **curler name** instead of the curler ID when viewing a curler's stats from the team stats page.
- **Thinking time by position: curlers and averages** – On the thinking-time drill-down page, under each position (Lead, Second, Vice, Skip) the app now lists the **curlers** who played that position and their **total and average** thinking time per rock.
- **Stats games list: result column** – The games list (from **Games played** on team stats) now includes a **Result** column showing W, L, or T for each game based on total end scores.
- **Hammer drill-down: new column and percentages** – The per-game hammer breakdown page now has a **Scored 2+ w/ hammer** column (count of ends with hammer where we scored 2 or more). The **Scored** column is renamed to **Scored w/ hammer**. Each game row shows **hammer efficiency %**, **steal defense %**, **force efficiency %**, and **steal efficiency %** next to the corresponding raw counts.

### Changed

- **Hammer stats column label** – "Scored" is now "Scored w/ hammer" on the hammer drill-down table.

---

## [1.4.3] - 2026-02-16

### Changed

- **Breadcrumb on team and stats pages** – The dashboard breadcrumb now shows the **team name** instead of the team ID (UUID) when viewing a team or its stats (e.g. Teams > Team Name > Stats > hammer). Team name is resolved via a lightweight server action and cached for the session when navigating between sub-pages.

---

## [1.4.2] - 2026-02-16

### Added

- **Thinking time by team and position (score page)** – When any end has thinking time data, the score page shows a **Thinking time by team and position** card at the bottom with our team and opponent breakdowns by Lead/Second/Vice/Skip (standard) or Position 1–2 (doubles), using the single pair set and hammer per end.

### Changed

- **Thinking time: single set per end** – Thinking time is now stored as one set of 15 pairs (standard) or 9 pairs (doubles) per end. Pairs 1–3 = Lead, 4–7 = Second, 8–11 = Vice, 12–15 = Skip (standard); pair 1 = Position 1, pairs 2–7 = Position 2, pairs 8–9 = Position 1 (doubles). Hammer and non-hammer alternate; the non-hammer lead's first shot is not tracked. One textarea per end replaces separate "Our team" and "Opponent" fields; our team vs opponent is determined by `hammer_team` per end. Team and curler stats (thinking time by position) use this mapping. Game CSV export uses one array per end with an optional header describing the pair order; import accepts the same 15- or 9-pair order.
- **Documentation** – Changelog, backlog, administrator guide, end-user guide, test scripts, UAT/SIT spec, and sample_timestamps.txt updated for single thinking time set per end and score-page summary.

---

## [1.4.1] - 2026-02-16

### Added

- **Games tab by season** – The Games page now shows games grouped by season in cards. Each card is a season (or "Ad-hoc / No season" for games without a season). Games within each card are ordered by date, most recent first.
- **Game summary statistics** – On the game detail page, when the game has shots scored or the scoreboard has been updated, a **Game statistics** card appears with a per-curler table (Curler, Shots, Score, Avg %) and optional score summary. Shows "No shots scored yet" when only the scoreboard has been updated.
- **Dashboard Seasons link** – The dashboard quick links now include a **Seasons** button that goes to the Seasons list.
- **Seasons → games list** – From the Seasons list you can open a season and use **View games in this season** to see all games in that season (date-ordered). Season list cards also have a **View games** link. New route: `/seasons/[id]/games`.

### Changed

- **Documentation** – README, administrator guide, end-user guide, backlog, test scripts, and UAT/SIT spec updated for 1.4.1.

---

## [1.4.0] - 2026-02-16

### Added

- **Scoreboard fixes** – New ends now default to 0–0 (team and opponent score). The end-scores table uses equal width for both team name columns. **Delete end** is available in both the edit panel and the non-editing row (when the game has at least two ends) so owners can remove any end and renumber subsequent ends.
- **Statistics drill-downs** – **Games played** card links to a games list page (`/teams/[id]/stats/games`) for the same period. **With hammer** and **Without hammer** cards link to a per-game hammer breakdown page. **Thinking time by position** card links to a dedicated thinking-time drill-down page. All drill-down pages support the same period and game-format filters.
- **Curler stats: average thinking time by position** – On per-curler stats, when the curler has thinking time data, an **Average thinking time by position** section shows total and average thinking time for each position (Lead, Second, Vice, Skip) they played.
- **Clickable version opens Changelog** – The app version in the dashboard sidebar is now a button; clicking it opens the Changelog in a popup (same pattern as User Manual).
- **Opponent thinking time** – When thinking time is enabled for a game, the score page allows entering **opponent** thinking time per end (optional “Opponent” textarea alongside “Our team”). Team statistics show **Opponent** thinking time by position when recorded. Team stats CSV export includes opponent thinking time by position when available.
- **Edit game settings after creation** – Team owners and collaborators can edit game metadata from the game detail page via **Edit game**: location, sheet, YouTube stream URL (and optionally other fields). Changes are saved without affecting lineup or shot data.

### Changed

- **Documentation** – README, administrator guide, end-user guide, backlog, test scripts, and UAT/SIT spec updated for 1.4.0.

---

## [1.3.0] - 2026-02-16

### Added

- **Public team stats drill-down** – Viewers of a public team link (`/t/[slug]`) can open curler stats (`/t/[slug]/curlers/[curlerId]`), games list (`/t/[slug]/games`), game detail (`/t/[slug]/games/[gameId]`), and read-only shot-by-shot scoring (`/t/[slug]/games/[gameId]/score`). Curler names on the public team page link to per-curler stats; a "View games" link goes to the games list.
- **Team invitations** – Team owners can invite users by email from the team detail page. Invitees get full read access and can create games for that team without the team being public. If the invitee has an account, they are added as a collaborator; otherwise an accept link is generated. Accept/decline via link (`/accept-team-invite?token=...`) or from a "Pending team invitations" card on the Teams page.
- **Same-date game confirmation** – When creating a game on a date that already has a game for that team, a dialog asks: "Different game – create new", "Same game – open existing", or "Cancel".

### Changed

- **Team access** – Collaborators (invited team members) can view team data, create games, add/edit curlers, view stats, and export. Only the owner can edit team settings, delete the team, invite/remove members, or undo last end.

---

## [1.2.0] - 2026-02-16

### Added

- **Thinking time by position** – On team statistics, when any game in the selected period has thinking time recorded, a **Thinking time by position** section shows total and average thinking time per position (Lead, Second, Vice, Skip for standard; positions 1–2 for doubles). Only rocks 2–16 (standard) or 2–10 (doubles) are included; the lead's first stone of each end is not timed.
- **Curler stats by position** – On per-curler stats, a **By position** table shows games, ends, shots, shooting %, and normalized average when the curler played each lineup position (throwing order), so you can compare performance at Lead vs Vice, etc.
- **Team wins and losses** – Team statistics now show **Wins**, **Losses**, **Points for**, and **Points against** for the selected period. When applicable, **Tie-breaker games** W–L is also shown.
- **Hammer and without-hammer metrics** – Team stats include:
  - **With hammer:** Hammer efficiency % (ends with hammer where you scored 2+ when points were scored), Steal defense % (100% − stolen ends / ends with hammer), and a combined **Rating with hammer**.
  - **Without hammer:** Force efficiency % (ends opponent had hammer and scored only 1), Steal efficiency % (ends you stole / ends without hammer), and a combined **Rating without hammer**.
- **Trendlines** – The per-curler **Last N games** line chart now shows an optional linear trendline (dashed) for season and lifetime views to visualize direction of shot ratings over time.
- **Export and public stats** – Team stats CSV export includes record (wins, losses, points for/against, tiebreaker W–L), hammer/steal/force metrics, and thinking time by position when available. Public team stats page (`/t/[slug]`) now shows W–L, points for/against, and hammer/without-hammer metrics.

### Changed

- **Documentation** – README, administrator guide, end-user guide, backlog, test scripts, and UAT/SIT spec updated for 1.2.0.

---

## [1.1.11] - 2026-02-16

### Added

- **Delete end (with scores)** – On the game scoreboard, when editing an end, owners can delete that end and all its shot data via a **Delete end** button (with confirmation). Applies to any end when the game has at least two ends; subsequent ends are renumbered. Existing **Remove** / **Undo last end** remain for the last end when it has no scores.

---

## [1.1.10] - 2026-02-16

### Added

- **Mixed doubles (WCF): power play and hammer choice**  
  For games with **game format = Doubles**, you can now record **power play** per end (which end(s) it was used and by which team). When editing an end on the Scoreboard, optionally mark "Power play this end" and choose our team or opponent; the end row shows a "PP" indicator when used. In **new game** (doubles only), you can optionally set **Hammer in first end – how decided** (Coin toss / Decision / Not specified). New columns: `game_ends.power_play_used`, `game_ends.power_play_team`; `games.hammer_first_end_method`. Game and end CSV export/import include these fields.

---

## [1.1.9] - 2026-02-16

### Changed

- **Thinking time: show all YouTube links**  
  On the shot-by-shot scoring page, the **Links (YouTube)** section for each end now lists **all** rocks (2–16 for standard, 2–10 for doubles) with Start/Stop timestamp links. Previously only the first five rocks were shown with a “… and N more” summary; all links are now visible and clickable.

---

## [1.1.8] - 2026-02-16

### Changed

- **Shot-by-shot table: improved column contrast**  
  In **Simple** and **Both** modes, the shot grid now uses **stronger curler block background shading** and a **more visible vertical border** between blocks; **alternating shade on the six sub-columns** (Guard Out/In, Draw Out/In, Hit Out/In) improves readability in both light and dark theme. No change to data or behavior.

---

## [1.1.7] - 2026-02-16

### Added

- **Build-time app version**  
  The sidebar version (e.g. "v1.1.7") is now generated at **build time** from this Changelog (first line matching `## [X.Y.Z]`). The script `scripts/generate-app-version.js` runs automatically before `next build` (via `prebuild`). It writes `lib/app-version.generated.ts`, so the version is bundled and displays correctly in production without reading the filesystem at runtime.

- **Shot-by-shot table: column styling by curler**  
  In **Simple** and **Both** scoring modes, the shot-by-shot grid now uses **alternating background shading** and a **vertical border** between each curler’s column block so you can quickly see which columns belong to which curler. No change to data or behavior—purely visual.

### Changed

- **Version display**  
  The dashboard sidebar no longer reads `Documents/Changelog.txt` at runtime. It uses the value generated by the prebuild script instead, so the version always matches the Changelog at the time of build.

---

## [1.1.6] - 2026-02-16

### Added

- **App version in sidebar footer**  
  The dashboard sidebar shows the current app version at the bottom (e.g. "v1.1.6"). As of 1.1.7 the version is generated at build time from `Documents/Changelog.txt` via a prebuild script; see 1.1.7 for details.

- **Remove last end from the end-scores row**  
  When you are the game owner and the **last** end has no scores (and there are at least two ends), you can remove that end in two ways: the existing **Undo last end** button below the table, or a **Remove** button in the last row of the end-scores table. Only the last end can be removed; middle ends with no score cannot be removed.

### Changed

- **Tiebreaker label on game page**  
  When a game is complete and at least one end was marked as a tiebreaker, the game header score now shows e.g. "5–4 (tiebreaker)" instead of "5–4". "In progress" is unchanged when the game is not complete.

- **Simple shot grid: 2 rows per end (standard), 3 rows (doubles)**  
  The simple scoring grid now uses **two data rows per end** for standard games (Shot 1 = first throw of each position [shots 1, 3, 5, 7]; Shot 2 = second throw [shots 2, 4, 6, 8]) and **three data rows per end** for doubles (Shot 1 = shot 1 curler 1, shot 2 curler 2; Shot 2 = shot 3 curler 2 only; Shot 3 = shot 4 curler 2, shot 5 curler 1). Row labels in the first column show "End N · Shot 1", "Shot 2", and (doubles) "Shot 3". Export and API still use `end_number` and `shot_number`; mapping is unchanged.

---

## [1.1.5] - 2026-02-16

### Added

- **Tiebreaker / skip's rocks**  
  Any end can be marked as a **tiebreaker** (e.g. draw to the button). When editing that end, scores are restricted to **0–0**, **1–0**, or **0–1** only. The Scoreboard shows "(TB)" next to the end number for tiebreaker ends. The `game_ends` table has a new column **`is_tiebreaker`** (boolean, default false). Game CSV and JSON export/import include `is_tiebreaker`.

### Changed

- **Unified end scores table**  
  The Scoreboard card now has a **single end-scores table** instead of two separate entry methods. Below the club-format scoreboard (point totals 1–16), one table lists each end in a row with columns: **End** | **Our team** | **Opponent** | **Edit** (or Tiebreaker + Save/Cancel when editing). The number of ends is driven by game setup (Number of ends on the game creation page); extra ends (beyond that number) are labeled "(extra)". The previous inline "Enter scores" row and the separate **End scores** card have been removed.

---

## [1.1.4] - 2026-02-16

### Fixed

- **Simple scoring table header/row alignment**  
  The simple scoring grid now uses a flattened body (one table cell per category-turn column) and consistent column widths (`table-fixed` with a fixed End column) so that the Guard Out/In, Draw Out/In, and Hit Out/In headers align correctly with the data cells below.

### Changed

- **Shot order: each curler's two shots in a row**  
  Throwing order is now consecutive per curler: shots 1–2 = position 1 (curler 1), shots 3–4 = position 2, shots 5–6 = position 3, shots 7–8 = position 4. This applies to lineup lookup (who threw which shot), the detailed shot form, and the simple grid. Doubles order is unchanged (alternating positions).

- **Simple scoring grid layout (standard games)**  
  For standard 4-person games, the simple grid shows **eight rows per end** (one row per shot), with each curler's two shots in consecutive rows. Columns remain one block per player with six cells (Guard Out/In, Draw Out/In, Hit Out/In); only the block for the curler who throws that shot is active in each row.

- **Default scoring mode**  
  Opening the score page (`/games/[id]/score`) without a `mode` query now **defaults to Simple** (the tabular grid). Use the Scoring mode toggle to switch to Detailed or Both.

---

## [1.1.3] - 2026-02-16

### Added

- **Club-style scoreboard**  
  The game scoreboard uses the club format: columns are **point totals 1–16** (not end numbers). Numbers in cells are **end numbers** (which end produced that total). Column 16 is used for **blank ends** (0–0). The table has three rows: our team (rock color), a middle row with column labels 1…16, and the opponent (rock color). No API or schema changes.

- **Direct end-score entry on the scoreboard card**  
  You can enter or edit end scores (our/opponent 0–8) **directly on the Scoreboard card**: an inline row below the club table shows each end with two inputs (our score, opponent score) and **Save**. The separate **End scores** card remains available for users who prefer the list view.

---

## [1.1.2] - 2026-02-16

### Added

- **Generic shot types (Hit, Draw, Guard)**  
  The database now includes category-level shot types **Hit**, **Draw**, and **Guard** (migration `add_generic_shot_types`). These appear first in each category in the detailed shot-type dropdown and are used by the simple scoring grid.

- **Scoring mode toggle on score page**  
  The shot-by-shot score page (`/games/[id]/score`) supports three modes, selectable via a toggle (and URL `?mode=detailed|simple|both`):
  - **Detailed** – Existing form: one shot at a time with full shot type list, turn, score, notes, and rubric.
  - **Simple** – Tabular grid by end and player: columns are Guard Out/In, Draw Out/In, Hit Out/In; click a cell and enter a score (0–scale max). Same data is stored as in detailed mode.
  - **Both** – Simple grid first, then the detailed form below so you can enter quickly in the grid and optionally refine any shot (e.g. change to a specific type or add notes).

- **Simple tabular scoring grid**  
  In Simple or Both mode, a table shows ends as rows (with two shot sub-rows per end for standard games; three for doubles). Each player has six cells (Guard Out, Guard In, Draw Out, Draw In, Hit Out, Hit In). Entering a score in a cell saves that shot with the corresponding generic type and turn. Existing shots are shown in the correct cell; the grid works with the same lineup and shot data as the detailed form.

- **End-level shooting percentage in simple grid**  
  Under each end in the simple scoring grid, a row displays that end’s shooting percentage (sum of scores / (count × scale max) × 100). Shown as "—" when the end has no shots.

### Changed

- **Detailed shot form: Hit, Draw, Guard first in dropdown**  
  In the shot type dropdown, the generic **Hit**, **Draw**, and **Guard** options now appear first within their category (before specific types such as Hit & Stick, Freeze), so users can choose category-level or specific type in one list.

---

## [1.1.1] - 2025-02-15

### Fixed

- **Score page crash**  
  Resolved "Cannot access before initialization" on the shot-by-shot score page (`/games/[id]/score`). Declaration order in the ShotEntry component was corrected so `initialForm` is defined before it is used when computing `selectedShotType` and rubric content.

### Changed

- **End score entry defaults**  
  When entering or editing end scores on the game scoreboard, the default value for both "Our score" and "Opponent score" is now **0** instead of blank. Users can still leave a field blank to indicate no score.

- **New game: number of ends**  
  New games already default to 8 ends (form, validation, and server); no code change was required. Documented here for clarity.

---

## [1.1.0] - 2025-02-15

### Added

- **User-owned seasons**
  - Seasons are owned by the signed-in user (no longer tied to a single team). Create a season from the Seasons page with name and date range; no team required.
  - **Seasons** page lists "My seasons" (owned) and "Seasons you're invited to" (collaborator). Each links to a season manage page.

- **Multiple teams per season**
  - Teams are linked to seasons via a junction table. Season owner and collaborators can add their own teams to a season. From a team’s detail page, "Manage seasons" shows "Seasons this team is in" with options to add the team to an existing season or create a new season and add it.
  - When creating or editing a game, the Season dropdown shows only seasons that include the selected team.

- **Season collaborators and invitations**
  - Season owner can invite other users by email. If the user has an account, they are added as a collaborator immediately. Collaborators can add their teams to the season and view season stats (including shared curler stats).
  - **Pending invitations**: If the invitee has no account, a pending invitation is created and the owner sees a **Copy link** to share. Invite link format: `/accept-invite?token=...`.

- **Accept-invite page and sign-up from invite link**
  - Public `/accept-invite` page: invitee opens the link, sees the season name, and can **Sign up** or **Sign in** (with redirect back to accept). After signing in, they click **Accept invitation** to join the season.
  - **Pending invitations** card on the Seasons page: invitees see pending invites and can **Accept** or **Decline**.

- **Season stats and shared curlers**
  - Season stats view (from season manage page): lists teams in the season with summary stats and a **Shared curlers** section (curler identities with combined stats across teams in the season). Visible to owner and collaborators.

- **Export/import and admin**
  - Export includes seasons (with owner_id), team_seasons, and season_collaborators. Import recreates seasons and links; supports legacy payloads.
  - Admin: delete team removes team_seasons rows (no longer deletes seasons by team). Delete user removes season_collaborators and seasons they own.

- **Administrator Guide**
  - Sidebar link for admins opens the Administrator Guide in a popup (same pattern as User Manual). Content from Documents/administrator-guide.md.

### Changed

- **Seasons data model**: Removed `seasons.team_id`. Added `seasons.owner_id`, `team_seasons`, and `season_collaborators` tables. Added `season_invitations` for pending invites with token-based accept links.
  - Game validation: a game may use a season only if (team_id, season_id) exists in team_seasons.
  - Public team page: seasons for the team are resolved via team_seasons.

- **Documentation**
  - End-user guide, administrator guide, test scripts, and UAT spec updated for user-owned seasons, collaborators, pending invitations, and sign-up from invite link. User manual and admin guide popups reflect the same content.

---

## [1.0.0] - (prior)

Initial application release: teams, curlers, games, shot-by-shot scoring, team and curler statistics, export/import, public team share, multiple scorers, Clerk auth, Supabase backend.