diff --git a/.agent/phases/todo/65_bottom_chat_actions/00_phase.md b/.agent/phases/todo/65_bottom_chat_actions/00_phase.md
new file mode 100644
index 0000000..21a1489
--- /dev/null
+++ b/.agent/phases/todo/65_bottom_chat_actions/00_phase.md
@@ -0,0 +1,48 @@
+# Phase 65 — Chat Action Cluster to the Pinned Bottom
+
+**Source:** `TODO.md` L3 — "Move the new chat and share button to the tune/retry/save doc cluster area so it's always at the bottom of the screen and easily accessble. Make the button clusters look better, neater, more aligned"
+**Story:** n/a (TODO-derived — owner roadmap confirmation 2026-09-01)
+**Context:** Verified layout inventory (audited 2026-09-01, line numbers as of this writing):
+- `frontend/index.html` — the `.chat-actions` row (New chat + Share pills, ONE wrapper) sits at the **TOP** of the 46rem chat column: the comment block runs L126–148, the row div is L149, positioned between `#steering-announcer` (L129) and `` (L193). The pinned composer form follows the messages at L225. In any real conversation the row scrolls off-screen the moment the reader is at the bottom — the exact defect this phase removes.
+- `frontend/assets/styles.css` — `.chat-actions` (L433: horizontal row, `align-items: center`, `gap: 0.6rem`, intrinsic pill widths); `.new-chat-btn` (L301) / `.share-chat-btn` (L335) — solid brand pill family, `min-height: 44px`, `border-radius: 999px`; `.composer` (L1226) — `position: sticky; bottom: env(safe-area-inset-bottom, 0)`, no z-index (the phase-52 pin, owner-locked 2026-08-30); `.messages` — `flex: 1 1 auto` + `min-height: 200px` (the grow that rests the bottom at the screen on short chats); the per-answer meta cluster: `.msg-meta` (L641), `.tune-btn` (L694), `.retry-btn` (L723), `.save-as-doc-btn` (L756) — ghost pill family, `min-height: 44px`, 14px icons, `margin(-inline-start): auto` pushes Tune/Retry/Save-as-doc to the row's **right edge** ("Save as doc" is the bottom-right action of every completed brain bubble — the app's action corner). The ≤640px block (L3049) flips `.chat-actions` to a full-width vertical stack.
+- `frontend/assets/header.js` L336–339 — binds `#new-chat-btn` and dispatches the `bor:new-chat` window event; `frontend/assets/app.js` L1700 listens and resets the conversation (it owns the in-flight-turn guard); `app.js` L246/L2067 binds `#share-chat-btn` to `shareCurrentChat` (the empty-chat no-op writes `Nothing to share yet.` to `#send-status`). All bindings are id-based and **position-independent** — the move is pure HTML/CSS.
+- `frontend/assets/app.js` `copyShareLinkWithFallback` (L1424) appends the clipboard-fallback field to the **composer** (not the row) — unaffected by the move.
+- Existing suites that constrain the design: `tests/e2e/test_pinned_composer.py` (the `#composer` computed `position` must stay `sticky`; the composer box flush with the viewport bottom at EVERY scroll position inside the sticky range; settles back into flow above the footer at the document bottom; NO z-index on the composer; the empty chat must not be scrollable, `sh <= ch + 1`); `tests/e2e/test_save_share_ux.py::test_action_row_layout` (ONE `.chat-actions`, DOM order New chat → Share, horizontal row at 1280px with intrinsic pill widths, full-width stack at 390px, no horizontal overflow at 360px — all RELATIVE geometry, nothing pins the row's vertical position); `tests/e2e/test_chat_persistence.py` (`#new-chat-btn` ≥44px, click resets the conversation, `New chat started` in `#send-status`).
+- `.agent/` is tracked and committed (AGENTS.md rule 8) — the phase commit stages `.agent/` + `frontend/` + `tests/`.
+
+## Objective
+The New chat + Share pills live in the **bottom cluster** of the chat column — the same screen zone as the tune/retry/save-doc meta actions — and are **always at the bottom of the screen** (pinned with the composer, visible even while scrolled up reading a long conversation). The two button clusters read as neat, aligned sets: the bottom row hugs the column's right edge, mirroring the right-aligned "Save as doc" corner above it.
+
+## Dependencies
+- `64_sync_upload_progress` (todo, preceding — no functional dependency; ordering by number)
+
+## Tasks
+1. `01_move_chat_actions_to_bottom.md` — relocate the `.chat-actions` row (buttons + comments byte-identical) to the last child of `.chat-shell`, directly above the composer; in-flow at first.
+2. `02_pin_bottom_cluster.md` — wrap row + composer in a sticky `.chat-bottom` unit (A1): the cluster is pinned at the viewport bottom at every scroll position and settles into flow above the footer at the document bottom.
+3. `03_align_button_clusters.md` — right-align the bottom row to the column's right edge (A2); verify the five action pills share one geometry (44px targets, 999px radius, global focus-visible).
+4. `04_e2e_bottom_chat_actions.md` — the dedicated Playwright suite `tests/e2e/test_bottom_chat_actions.py` (run in isolation): pinned at every scroll position, resting at the screen bottom, settled above the footer, geometry + DOM order, mobile stack, both buttons still work.
+5. `05_regressions_and_commit.md` — regression E2E matrix, full `validate.sh`, one atomic commit.
+
+## Testing & Quality
+- E2E (mandatory, house rule — one file per phase, run in isolation): `tests/e2e/test_bottom_chat_actions.py` against the shared conftest server (DB up, mock LLM, seeded KB — the `test_pinned_composer.py` patterns for overflow conversations and scroll measurement).
+- Frontend-only phase: `app/` is untouched, so the coverage gate stays satisfied at its current level — `validate.sh` (unit + integration >90% + ruff + pyright) must still pass end to end.
+- Regression E2E (each in isolation): `test_pinned_composer.py`, `test_save_share_ux.py`, `test_chat_persistence.py`, `test_share_chat.py`, `test_chat_history.py`, `test_smoke.py`.
+
+## Completion Criteria
+- [ ] The `.chat-actions` row is the bottom cluster of `.chat-shell` — above the composer, inside the sticky `.chat-bottom` — with DOM order New chat → Share preserved and all ids/aria-labels intact.
+- [ ] Over-viewport conversation, scrolled to the top: the row is fully visible, pinned at the viewport bottom above the composer, at every scroll position inside the sticky range; at the document bottom the cluster settles into flow above the footer (no overlap).
+- [ ] Empty chat at 1280×800: the row rests at the bottom of the screen, the document is not scrollable.
+- [ ] Desktop: the row's right edge aligns with the column's right edge (A2); ≤640px: full-width vertical stack; no horizontal overflow at 360px.
+- [ ] `#new-chat-btn` click resets the conversation (`New chat started` in `#send-status`); `#share-chat-btn` on an empty chat is a no-op (`Nothing to share yet.`) — the bindings survived the move.
+- [ ] `uv run pytest` green; coverage TOTAL >90%; `uv run ruff check . && uv run pyright` clean.
+- [ ] `uv run pytest tests/e2e/test_bottom_chat_actions.py -v --no-cov` green in isolation (DB up); the six regression E2E suites green in isolation.
+- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
+
+## Locked decisions
+- **Owner-locked (2026-09-01, roadmap confirmation, A1):** *pinned bottom cluster* — a `.chat-bottom` wrapper (`position: sticky; bottom: env(safe-area-inset-bottom, 0)`, NO z-index) groups `.chat-actions` + `#composer` as one sticky unit, so the pills are literally always at the bottom of the screen — the rejected alternative (plain in-flow row) would scroll away while reading up a long conversation. The `#composer`'s own sticky declaration STAYS (redundant inside the wrapper — it cannot shift within its containing block — but `test_pinned_composer.py` pins the computed style `sticky`).
+- **Owner-locked (2026-09-01, roadmap confirmation, A2):** *right-aligned bottom row* — the New chat + Share row hugs the column's RIGHT edge on desktop (so the bottom-right reads as one aligned action column with the right-aligned "Save as doc" buttons above); the ≤640px stack stays full-width (alignment is moot when stretched).
+
+## Commit
+```bash
+git add .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(web): move the chat action cluster to the pinned bottom and align the button sets"
+```
diff --git a/.agent/phases/todo/65_bottom_chat_actions/01_move_chat_actions_to_bottom.md b/.agent/phases/todo/65_bottom_chat_actions/01_move_chat_actions_to_bottom.md
new file mode 100644
index 0000000..5f74e54
--- /dev/null
+++ b/.agent/phases/todo/65_bottom_chat_actions/01_move_chat_actions_to_bottom.md
@@ -0,0 +1,28 @@
+# Task 01 — Move the `.chat-actions` row to the bottom of the chat column
+
+**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "Move the new chat and share button to the tune/retry/save doc cluster area so it's always at the bottom of the screen and easily accessble…"
+**Story:** n/a (TODO-derived)
+
+## Objective
+The New chat + Share row leaves the TOP of the column (where it scrolls off-screen the moment a conversation grows) and becomes the last child of `.chat-shell`, directly above the pinned composer — the same screen zone as the tune/retry/save-doc meta cluster. This task is the MOVE only (in-flow); the sticky pin lands in task 02 and the alignment in task 03.
+
+## Work
+1. `frontend/index.html`:
+ - Cut the ENTIRE `.chat-actions` block — the comment at L126–148 PLUS the `…
` at L149–187 (both buttons, every line of their comments included) — from between `#steering-announcer` (L129) and `` (L193).
+ - Paste it as the LAST child of `.chat-shell`: between the messages `` (L207) and the composer comment block (L209), so each comment stays glued to its own element and the form (L225) is untouched. Indent to match the sibling elements (6 spaces); keep the blank-line separation between the four children.
+ - The two buttons stay BYTE-IDENTICAL: ids (`#new-chat-btn`, `#share-chat-btn`), classes, `aria-label`s, SVGs, the label spans, and the DOM order **New chat → Share** (pinned by `test_save_share_ux.py::test_action_row_layout`).
+ - Update the row's comment block in place (it moves with the row): keep the Phase 55/14/51 history, and APPEND a Phase 65 note — the row was relocated from the top of the column to the bottom (above the composer) per `TODO.md` L3 (owner confirmation 2026-09-01), so the cluster sits where the tune/retry/save-doc meta actions live; the top of the column is now banner → steering → announcer → messages.
+2. No CSS changes in this task: the row's styling is position-independent (`.chat-actions` at `styles.css` L433; the ≤640px stack rule at L3049). As a column child it inherits `.chat-shell`'s `gap: 1rem` above (from `.messages`) and below (to the composer) — acceptable until task 02/03 set the wrapper gap.
+3. No JS changes: every binding is id-based and position-independent — `frontend/assets/header.js` L336–339 (`#new-chat-btn` → `bor:new-chat` dispatch), `frontend/assets/app.js` L1700 (the listener), L246 + L2067 (`#share-chat-btn` → `shareCurrentChat`). The clipboard-fallback field already appends to the composer (`app.js` L1424), not the row.
+4. Leave UNTOUCHED: `#kb-banner`, `#stale-banner`, `#steering-panel`, `#steering-announcer`, `#messages`, `#composer`, and every comment except the row's own moved block.
+
+## Testing & Quality
+- `uv run pytest` green (unit + integration — no Python changed, gate stays green); coverage TOTAL **>90%**; `uv run ruff check . && uv run pyright` clean.
+- Highest-risk existing suite for this move: `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation (DB up) — it pins the row's geometry (single `.chat-actions`, DOM order, horizontal/stacked, intrinsic widths, 360px overflow) but nothing about its vertical position, so the move must not perturb it.
+
+## Completion Criteria
+- [ ] In `frontend/index.html`, the `.chat-actions` div appears AFTER `` (the messages close, L207) and immediately before the composer form — it is the last child of `.chat-shell`; nothing remains at the old position (grep: exactly one `.chat-actions` in the file).
+- [ ] `grep -n "new-chat-btn\|share-chat-btn" frontend/index.html` shows both buttons with unchanged ids/classes/aria-labels, New chat before Share.
+- [ ] The row's comment block moved with the row and carries the Phase 65 relocation note.
+- [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.
+- [ ] `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation (DB up).
diff --git a/.agent/phases/todo/65_bottom_chat_actions/02_pin_bottom_cluster.md b/.agent/phases/todo/65_bottom_chat_actions/02_pin_bottom_cluster.md
new file mode 100644
index 0000000..9589146
--- /dev/null
+++ b/.agent/phases/todo/65_bottom_chat_actions/02_pin_bottom_cluster.md
@@ -0,0 +1,46 @@
+# Task 02 — Pin the bottom cluster (`.chat-bottom` sticky unit)
+
+**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "…so it's always at the bottom of the screen and easily accessble."
+**Story:** n/a (TODO-derived)
+
+## Objective
+The row + composer become ONE pinned unit (locked A1): at every scroll position inside the sticky range the bottom cluster sits flush with the viewport bottom — the pills are literally always at the bottom of the screen, even while the reader is scrolled up through a long conversation — and at the document bottom the cluster settles back into normal flow above the footer (never floating over it).
+
+## Work
+1. `frontend/index.html`:
+ - Wrap the `.chat-actions` div (task 01's position) AND the `
` (incl. its comment block) in a new `` … `
` that is the single LAST child of `.chat-shell` (before the shell's closing ``, currently L237). No id (nothing in JS binds it — the bindings are on the inner elements).
+ - Append a Phase 65 note to the row's comment (A1): the wrapper makes row + composer one sticky unit — `position: sticky; bottom: env(safe-area-inset-bottom, 0)`, NO z-index (the sticky header stays on top — the phase-46 stacking pinned by `test_pinned_composer.py`); the composer's OWN sticky declaration is kept (redundant inside the wrapper — its containing block is the wrapper, so it cannot shift — but `test_pinned_composer.py` asserts the computed style `sticky` on `#composer`).
+2. `frontend/assets/styles.css` — new rule directly above the `.composer` rule (L1226):
+ ```css
+ /* Phase 65 (owner-locked A1, 2026-09-01, TODO.md L3): the pinned
+ bottom cluster — the .chat-actions row + the #composer form as
+ ONE sticky unit ... (full house-style rationale: sticky range =
+ the .chat-shell containing block; the .messages flex-grow still
+ rests the unit at the screen bottom on short chats; no z-index,
+ so the phase-12 sticky header (z 20) and the phase-46 dropdown
+ always stay on top; the composer's own sticky rule stays —
+ redundant here, pinned by tests/e2e/test_pinned_composer.py). */
+ .chat-bottom {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ position: sticky;
+ bottom: env(safe-area-inset-bottom, 0);
+ }
+ ```
+ - `gap: 0.5rem` is the row↔composer spacing INSIDE the wrapper (the wrapper is one column child, so `.chat-shell`'s 1rem gap applies only ABOVE it, from `.messages`).
+ - The `#composer` rule itself is NOT modified (its `position: sticky; bottom: env(...)` stays — see the note above).
+ - The ≤640px block needs no change: the wrapper is a column flex box, so the stacked full-width pills (the existing L3049 `.chat-actions` rule) simply make the unit taller on phones.
+3. Update the `.composer` rule's phase-52 comment minimally: the pin now belongs to `.chat-bottom`; the composer's own declaration is retained for the computed-style test pin (one line, no deletion of the phase-52 history).
+4. Leave UNTOUCHED: `.messages` (`flex: 1 1 auto` + `min-height: 200px` — the grow that absorbs free space on short chats still works: the wrapper is the last child, so the grow pushes the WHOLE unit down), every z-index in the file, and all JS.
+
+## Testing & Quality
+- `uv run pytest` green; `uv run ruff check . && uv run pyright` clean; coverage TOTAL **>90%** (gate stays green — no Python changed).
+- The load-bearing regression: `uv run pytest tests/e2e/test_pinned_composer.py -v --no-cov` green in isolation (DB up) — it re-verifies the whole pin contract WITH the wrapper: composer box flush with the viewport bottom at every scroll position, settled into flow above the footer at the document bottom, `#composer` computed `position === "sticky"` and `zIndex` in {auto, 0}, under the 64px header on a 375×812 phone, and the empty chat still not scrollable (`sh <= ch + 1` — the wrapper adds no height: the row already existed in the column, task 01).
+- `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation (row geometry unchanged by the wrapper).
+
+## Completion Criteria
+- [ ] `frontend/index.html`: exactly one `.chat-bottom` div, the last child of `.chat-shell`, containing exactly `.chat-actions` then `#composer` (in that order).
+- [ ] `frontend/assets/styles.css`: the `.chat-bottom` rule above `.composer` with `position: sticky; bottom: env(safe-area-inset-bottom, 0)` and NO z-index declaration; the `#composer` rule unmodified.
+- [ ] Browser check via `test_pinned_composer.py` green in isolation — in particular `test_composer_pinned_at_every_scroll_position` (flush at 0/25/50/75% of the sticky range), `test_empty_chat_composer_sits_at_the_screen_bottom` (no scrollable space), `test_pin_holds_on_mobile_under_the_header` (no header overlap, no z-index).
+- [ ] `test_save_share_ux.py` green in isolation.
diff --git a/.agent/phases/todo/65_bottom_chat_actions/03_align_button_clusters.md b/.agent/phases/todo/65_bottom_chat_actions/03_align_button_clusters.md
new file mode 100644
index 0000000..dd12333
--- /dev/null
+++ b/.agent/phases/todo/65_bottom_chat_actions/03_align_button_clusters.md
@@ -0,0 +1,27 @@
+# Task 03 — Align the two button clusters (neater, more aligned)
+
+**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "Make the button clusters look better, neater, more aligned"
+**Story:** n/a (TODO-derived)
+
+## Objective
+The bottom row and the per-answer meta cluster read as ONE tidy, aligned system (locked A2): the New chat + Share row hugs the column's RIGHT edge on desktop — mirroring the right-aligned "Save as doc" action of every brain bubble above it — so the bottom-right of the chat is a single aligned action column. The five action pills share one geometry (≥44px targets, 999px radius, the global focus-visible ring).
+
+## Work
+1. `frontend/assets/styles.css` — the `.chat-actions` rule (L433): add `justify-content: flex-end;` so the row hugs the column's right edge on desktop. The pills keep their intrinsic widths (never stretched — `align-items: center` already beats the column default, and `justify-content` only shifts the group).
+ - The ≤640px block (L3049: `.chat-actions { flex-direction: column; align-items: stretch; gap: 0.5rem; }`) is UNCHANGED — with stretched full-width pills, horizontal alignment is moot (the stacked pills already edge-to-edge the column).
+ - Update the rule's comment: the row is the bottom cluster's top member (task 01/02), right-aligned per A2, and the vertical row↔composer spacing is the `.chat-bottom` wrapper's 0.5rem gap (task 02).
+2. Five-pill geometry pass (VERIFY — fix only if a deviation exists; the expectation is "no declaration changes"):
+ - `.new-chat-btn` (L301) / `.share-chat-btn` (L335): solid brand family — `min-height: 44px`, `border-radius: 999px`, `border: 0`, 16px icons (hidden on desktop, icon-only ≤640px), `white-space: nowrap`.
+ - `.tune-btn` (L694) / `.retry-btn` (L723) / `.save-as-doc-btn` (L756): ghost family — `min-height: 44px`, `border-radius: 999px`, `padding: 0.35rem 0.8rem`, 14px icons, `margin-left/auto` right-alignment, `font-size: 0.82rem`.
+ - If any pill measures <44px tall at 360–1280px (deviation), bring it to the family's `min-height: 44px` here — do not invent new visual languages; the two families (solid = chat-level actions, ghost = per-answer actions) are intentional (phase 55 A2).
+3. No HTML changes, no JS changes, no changes to the meta-row layout (`.msg-meta` L641 stays as-is — its internal order, chips-then-actions, is pinned by the phase 49/59 comment history).
+4. No new CSS beyond the one declaration + comments.
+
+## Testing & Quality
+- `uv run pytest` green; `uv run ruff check . && uv run pyright` clean; coverage TOTAL **>90%** (gate stays green).
+- `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation — its desktop assertions (Share right of New chat, both pills < column/2 wide, ONE `.chat-actions`) hold for a right-aligned row; the right-EDGE alignment itself is pinned by this phase's suite in task 04.
+
+## Completion Criteria
+- [ ] `.chat-actions` rule carries `justify-content: flex-end;` with the A2 rationale in its comment; the ≤640px stack rule is byte-identical.
+- [ ] All five action pills (New chat, Share, Tune, Retry, Save as doc) report ≥44px rendered height at 360px, 390px, and 1280px widths (quick manual/Playwright measurement during the task; the standing pin lands in task 04).
+- [ ] `test_save_share_ux.py` green in isolation (DB up).
diff --git a/.agent/phases/todo/65_bottom_chat_actions/04_e2e_bottom_chat_actions.md b/.agent/phases/todo/65_bottom_chat_actions/04_e2e_bottom_chat_actions.md
new file mode 100644
index 0000000..232356d
--- /dev/null
+++ b/.agent/phases/todo/65_bottom_chat_actions/04_e2e_bottom_chat_actions.md
@@ -0,0 +1,43 @@
+# Task 04 — Dedicated E2E: `tests/e2e/test_bottom_chat_actions.py`
+
+**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "…so it's always at the bottom of the screen and easily accessble. Make the button clusters look better, neater, more aligned"
+**Story:** n/a (TODO-derived)
+
+## Objective
+One dedicated Playwright suite (house rule: one file per phase, run in isolation) pins the NEW contract — the bottom cluster is always at the bottom of the screen, right-aligned, still stacked/clickable — so a future refactor cannot silently move the pills back to the top.
+
+## Work
+1. `tests/e2e/test_bottom_chat_actions.py` (NEW — self-contained, house patterns):
+ - Header docstring: Source `TODO.md` L3, run-in-isolation command `uv run pytest tests/e2e/test_bottom_chat_actions.py -v --no-cov` (DB up: `podman compose up -d db`), the owner-locked A1/A2 contracts, and the test→contract mapping (house "Playwright Mapping Rule").
+ - Fixtures/helpers: reuse the conftest `page` (1280×800), `app_url`, `mock_llm`, `db_ready`; copy the house `_reset_db` (TRUNCATE `chunks, documents, query_log, steering_notes, saved_chats`) + threaded `_import_fixtures` KB-seed pattern from `tests/e2e/test_pinned_composer.py` (13 fixture docs) into a `seeded_kb` fixture; `login` from `tests/e2e/auth_helpers.py` is NOT needed (chat is public).
+ - Test 1 `test_empty_chat_row_rests_at_screen_bottom` (no KB seed needed — static markup):
+ - fresh `/`: exactly ONE `.chat-bottom`, the row inside it; the document is NOT scrollable (`scrollHeight <= innerHeight + 1` — the wrapper must not invent scrollable space);
+ - the row's box is in the LOWER part of the viewport (row bottom ≥ 75% of the viewport height) and sits directly ABOVE the composer (row bottom ≤ composer top + 4px, no overlap);
+ - A2: the row's RIGHT edge aligns with the `.chat-shell` right edge (±2px);
+ - the composer's bottom is not clipped below the viewport.
+ - Test 2 `test_bottom_cluster_pinned_at_every_scroll_position` (`seeded_kb`, the `test_pinned_composer.py` overflow pattern — 6 short grounded turns from that file's `SHORT_QUESTIONS`):
+ - assert the overflow precondition (`scrollHeight > innerHeight + 200`);
+ - compute the sticky range from the `.chat-bottom`'s CONTAINING BLOCK (`.chat-shell` document-bottom, same math as `test_pinned_composer.py::test_composer_pinned_at_every_scroll_position`) and assert `pin_limit > 200` and `max_scroll > pin_limit`;
+ - at scroll y ∈ {0, 25%, 50%, 75%, pin_limit−1}: the `.chat-bottom` box is FULLY inside the viewport with its bottom flush with the viewport bottom (±4px) — the row AND the composer are visible at every reading position (this is the TODO's "always at the bottom");
+ - at the document bottom: the cluster settles into flow — NO overlap with `.app-footer`, the composer is no longer glued to the viewport edge (composer bottom < viewport bottom − 4px), and the row is still directly above the composer.
+ - Test 3 `test_row_geometry_and_alignment` (no conversation needed for the row; one seeded turn for `.retry-btn`):
+ - exactly one `.chat-actions` inside `.chat-bottom`; exactly one `#new-chat-btn` + one `#share-chat-btn` in it; DOM order New chat → Share (`compareDocumentPosition` — same check as `test_save_share_ux.py`);
+ - desktop 1280×800: one horizontal row (overlapping y-bands), Share right of New chat, each pill's width < column width/2 (intrinsic, never stretched), row right edge == `.chat-shell` right edge (±2px, A2);
+ - mobile 390×844: vertical stack — Share BELOW New chat, both pills the same width, each == the `.chat-bottom` content width (±2px, full-width stretch);
+ - 360×800: `document.documentElement.scrollWidth <= 360` (no horizontal overflow);
+ - touch targets: `#new-chat-btn`, `#share-chat-btn` (and, after one seeded turn, the injected `.retry-btn` on the last brain bubble) all render ≥44px tall at BOTH 1280 and 390 widths.
+ - Test 4 `test_buttons_still_work_from_the_bottom` (`seeded_kb`, one turn so a real conversation exists):
+ - `#new-chat-btn` click → `.msg` count 0, `#empty-state` visible, `localStorage["bor.chat.v1"]` null, `#send-status` contains "New chat started" (the header.js → `bor:new-chat` → app.js chain survived the move);
+ - on the now-empty chat, `#share-chat-btn` click → `#send-status` contains "Nothing to share yet.", NO `.toast`, URL unchanged (the empty-conversation no-op guard, `app.js` L1494–1497).
+ - Determinism: mock-only answers (`MOCK_ANSWER_MARKER`), settled-state assertions (`wait_settled` pattern — `#send-label` back to "Send"), no scroll calls from the app itself (the phase-42 never-auto-scroll contract — the test does all scrolling via `window.scrollTo`).
+2. No other file changes in this task.
+
+## Testing & Quality
+- `uv run pytest tests/e2e/test_bottom_chat_actions.py -v --no-cov` green in isolation (DB up) — run it after EACH of the four tests is written, not all at once.
+- `uv run pytest` green (unit + integration — no Python app changed); `uv run ruff check . && uv run pyright` clean (the new test file must pass both).
+- Coverage TOTAL **>90%** (validate.sh gate — unchanged, no `app/` code).
+
+## Completion Criteria
+- [ ] The four tests exist, are named as specified, and are green in isolation (DB up).
+- [ ] The suite asserts the A1 pin (wrapper flush at every scroll position), the A2 right-edge alignment, the mobile stack, the 360px overflow bound, the DOM order, and BOTH click-through contracts (New chat reset, Share no-op).
+- [ ] Full suite green, lint + types clean, coverage >90%.
diff --git a/.agent/phases/todo/65_bottom_chat_actions/05_regressions_and_commit.md b/.agent/phases/todo/65_bottom_chat_actions/05_regressions_and_commit.md
new file mode 100644
index 0000000..314112d
--- /dev/null
+++ b/.agent/phases/todo/65_bottom_chat_actions/05_regressions_and_commit.md
@@ -0,0 +1,33 @@
+# Task 05 — Regression matrix, full gate, atomic commit
+
+**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "Move the new chat and share button to the tune/retry/save doc cluster area so it's always at the bottom of the screen and easily accessble. Make the button clusters look better, neater, more aligned"
+**Story:** n/a (TODO-derived)
+
+## Objective
+Prove the move + pin + alignment broke nothing in the adjacent contracts (the composer pin, the phase-55 action row, chat persistence, sharing, history) and land ONE atomic commit.
+
+## Work
+1. Regression E2E — each in isolation, DB up (run in this order; stop and fix the FIRST failure, do not batch-patch):
+ - `uv run pytest tests/e2e/test_pinned_composer.py -v --no-cov` (the composer pin — the most entangled contract with the new wrapper);
+ - `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` (the phase-55 row geometry + auto-save + share flows);
+ - `uv run pytest tests/e2e/test_chat_persistence.py -v --no-cov` (the New chat reset contract, incl. the ≥44px pin);
+ - `uv run pytest tests/e2e/test_share_chat.py -v --no-cov` (the full share lifecycle — the button moved under it);
+ - `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` (history restore + the Share button on the chat page);
+ - `uv run pytest tests/e2e/test_smoke.py -v --no-cov` (front-door sanity).
+2. The dedicated suite one final time: `uv run pytest tests/e2e/test_bottom_chat_actions.py -v --no-cov` green in isolation.
+3. Full gate: `uv run pytest` green; coverage TOTAL **>90%**; `uv run ruff check . && uv run pyright` clean (equivalently `.agent/validate.sh` exit 0).
+4. One atomic commit (`.agent/` is tracked — AGENTS.md rule 8):
+ ```bash
+ git add .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(web): move the chat action cluster to the pinned bottom and align the button sets"
+ ```
+ Commit body: one line per task — the move (task 01), the sticky `.chat-bottom` unit (task 02, A1), the right-edge alignment (task 03, A2), the dedicated suite (task 04), the regression outcome (task 05).
+
+## Testing & Quality
+- The six regression suites + the dedicated suite green in isolation (DB up).
+- `uv run pytest` green; coverage TOTAL **>90%**; lint + types clean.
+
+## Completion Criteria
+- [ ] All six regression suites green in isolation; `test_bottom_chat_actions.py` green in isolation.
+- [ ] `uv run pytest` green; coverage TOTAL >90%; `uv run ruff check . && uv run pyright` clean.
+- [ ] Exactly one new commit on `main` staging `.agent/ frontend/ tests/` only, `--no-gpg-sign`, Conventional Commits message as specified.
+- [ ] Phase dir moved to `.agent/phases/complete/65_bottom_chat_actions/` (pipeline step — `.agent/` stays tracked and committed).
diff --git a/.agent/phases/todo/66_history_auto_save_copy/00_phase.md b/.agent/phases/todo/66_history_auto_save_copy/00_phase.md
new file mode 100644
index 0000000..890356e
--- /dev/null
+++ b/.agent/phases/todo/66_history_auto_save_copy/00_phase.md
@@ -0,0 +1,45 @@
+# Phase 66 — History Tab Copy: Auto-Save, No Save Button
+
+**Source:** `TODO.md` L4 — "Update the history tab - the text still reads like there's a save button. There isn't anymore, every chat is saved."
+**Story:** n/a (TODO-derived — owner roadmap confirmation 2026-09-01)
+**Context:** Verified copy inventory (audited 2026-09-01, line numbers as of this writing):
+- `frontend/history.html` L6 — `` ("you saved" implies the manual action).
+- `frontend/history.html` L106–109 — the `.page-sub`: "Every conversation you pressed Save on — newest activity first. Click a title to return to that chat." — the primary offender: it names a Save button that has not existed since phase 55 (owner-locked A2, 2026-08-31: every conversation auto-saves via `app.js` `persistConversation`; pinned by `test_save_share_ux.py::test_anonymous_auto_save`).
+- `frontend/history.html` L163 — the empty-table row (inside `#history-empty-row`, `hidden` until `frontend/assets/history.js` reveals it on a 0-row fetch): "No saved chats yet — finish a conversation and press Save in the chat."
+- STAYS AS-IS (verified accurate — "saved" as a STATE, not a button): the `Saved chats
` (L105), the anonymous gate copy (L123–128: "Sign in to view your saved chats" / "Saved conversations are admin-only…"), the table caption, and the `history.js` aria-labels ("Delete saved chat: …", "Unshare saved chat: …").
+- No existing test pins any of the three offending strings (verified: "pressed Save" / "press Save" / "every conversation you saved" appear nowhere in `tests/`).
+- `.agent/` is tracked and committed (AGENTS.md rule 8) — the phase commit stages `.agent/` + `frontend/` + `tests/`.
+
+## Objective
+Every user-visible string on the History tab describes the app as it works TODAY: conversations save themselves automatically — there is no Save button to press. The exact replacement copy is locked below.
+
+## Dependencies
+- `65_bottom_chat_actions` (todo, preceding — no functional dependency; ordering by number)
+
+## Tasks
+1. `01_history_page_copy.md` — rewrite the three strings in `frontend/history.html` (meta description, page-sub, empty row) to the locked auto-save copy.
+2. `02_tests_and_commit.md` — unit text pins + dedicated E2E `tests/e2e/test_history_copy.py` (run in isolation) + regression pass + one atomic commit.
+
+## Testing & Quality
+- Unit pins (house style, `tests/unit/test_stale_ui_copy.py` pattern — read `frontend/history.html` as text): the old literals are GONE, the locked replacements are present.
+- E2E (mandatory, house rule): `tests/e2e/test_history_copy.py`, run in isolation against the shared conftest server (DB up) — an admin sees the ACTUAL page (gate hidden, table rendered): the rendered page-sub and empty-row texts match the locked copy, and no rendered page text reads like a Save button exists.
+- Coverage: **>90%** on `app/` (validate.sh gate — no `app/` change, the gate stays green at its current level).
+
+## Completion Criteria
+- [ ] `frontend/history.html` contains none of: "you pressed Save", "press Save", "every conversation you saved" — and carries the three locked (A3) strings.
+- [ ] The rendered History page (admin, empty table) shows the locked page-sub and empty-row text; the `Saved chats
` and the gate copy are untouched.
+- [ ] `uv run pytest` green; coverage TOTAL >90%; `uv run ruff check . && uv run pyright` clean.
+- [ ] `uv run pytest tests/e2e/test_history_copy.py -v --no-cov` green in isolation (DB up).
+- [ ] Regression E2E suites green in isolation: `test_chat_history.py`, `test_stale_saved_chats.py`.
+- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
+
+## Locked decisions
+- **Owner-locked (2026-09-01, roadmap confirmation, A3):** exact replacement copy —
+ - meta description (`history.html` L6): `Saved chats — every conversation is saved automatically, one click back.`
+ - page-sub (`history.html` L106–109): `Every conversation is saved automatically — newest activity first. Click a title to return to that chat.` (the `Save` emphasis is retired with the button — no inline emphasis in the replacement)
+ - empty row (`history.html` L163): `No saved chats yet — start a conversation and it will be saved automatically.`
+
+## Commit
+```bash
+git add .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(web): history tab copy — every chat saves automatically, there is no Save button"
+```
diff --git a/.agent/phases/todo/66_history_auto_save_copy/01_history_page_copy.md b/.agent/phases/todo/66_history_auto_save_copy/01_history_page_copy.md
new file mode 100644
index 0000000..d5b1851
--- /dev/null
+++ b/.agent/phases/todo/66_history_auto_save_copy/01_history_page_copy.md
@@ -0,0 +1,30 @@
+# Task 01 — Rewrite the History-tab copy to the auto-save model
+
+**Phase:** `66_history_auto_save_copy` · **Source:** `TODO.md:4` — "Update the history tab - the text still reads like there's a save button. There isn't anymore, every chat is saved."
+**Story:** n/a (TODO-derived)
+
+## Objective
+The three offending strings in `frontend/history.html` describe how BOR used to work (a Save button the visitor pressed). They become the locked (A3) auto-save copy — accurate, same house voice, no new markup.
+
+## Work
+1. `frontend/history.html` — three exact replacements (markup otherwise byte-identical):
+ - L6 — the meta description content becomes exactly (A3):
+ `Saved chats — every conversation is saved automatically, one click back.`
+ - L106–109 — the `.page-sub` becomes exactly (A3) — the `Save` emphasis is retired with the button; keep the `` element, the indentation, and the single paragraph:
+ `Every conversation is saved automatically — newest activity first. Click a title to return to that chat.`
+ - L163 — the `#history-empty-row` cell text becomes exactly (A3):
+ `No saved chats yet — start a conversation and it will be saved automatically.`
+ (the `colspan="6"`, the `hidden` attribute, and the row's id are untouched — `history.js` reveals this exact row on a 0-row fetch).
+2. Leave UNTOUCHED (verified accurate — "saved" as a state, not a button): the `
Saved chats
` (L105), the anonymous gate copy (L123–128), the table caption, every other row/cell in `history.html`, and everything in `frontend/assets/history.js` (its "saved chat" aria-labels are state language).
+3. No CSS, no JS, no other page — the TODO scopes this to the history tab; any OTHER page found to name a Save button is noted in the commit body as follow-up material, not fixed here.
+
+## Testing & Quality
+- `uv run pytest` green (unit + integration — no Python changed); `uv run ruff check . && uv run pyright` clean.
+- Coverage TOTAL **>90%** (validate.sh gate — unchanged, no `app/` code).
+- Quick manual sanity (optional, before the pins land in task 02): `grep -n "Save" frontend/history.html` — the only remaining case-sensitive "Save" hits are the retired-comment-free state strings ("Saved chats" h1, "Saved conversations are admin-only", "saved chats" table aria-label/caption).
+
+## Completion Criteria
+- [ ] `grep -c "pressed Save\|press Save\|every conversation you saved" frontend/history.html` → 0.
+- [ ] The three locked (A3) strings are present in `frontend/history.html` (meta, page-sub, empty row), each exactly once.
+- [ ] The h1, the gate section (L119–129), and `history.js` are byte-identical to before this task.
+- [ ] `uv run pytest` green; lint + types clean; coverage >90%.
diff --git a/.agent/phases/todo/66_history_auto_save_copy/02_tests_and_commit.md b/.agent/phases/todo/66_history_auto_save_copy/02_tests_and_commit.md
new file mode 100644
index 0000000..df2ad9b
--- /dev/null
+++ b/.agent/phases/todo/66_history_auto_save_copy/02_tests_and_commit.md
@@ -0,0 +1,41 @@
+# Task 02 — Unit pins + dedicated E2E + regression pass + atomic commit
+
+**Phase:** `66_history_auto_save_copy` · **Source:** `TODO.md:4` — "Update the history tab - the text still reads like there's a save button. There isn't anymore, every chat is saved."
+**Story:** n/a (TODO-derived)
+
+## Objective
+The new auto-save copy is pinned at two layers so it can never silently regress — unit text pins (no browser, house style) and one dedicated Playwright suite asserting what a signed-in admin actually SEES on the History tab — then one atomic commit.
+
+## Work
+1. `tests/unit/test_history_copy.py` (NEW — house pattern `tests/unit/test_stale_ui_copy.py`: read `frontend/history.html` as text, assert substrings):
+ - Negative pins (GONE): `"you pressed Save"`, `"press Save in the chat"`, `"every conversation you saved"`.
+ - Positive pins (PRESENT, exactly once each): the three locked (A3) strings — `Saved chats — every conversation is saved automatically, one click back.`, `Every conversation is saved automatically — newest activity first. Click a title to return to that chat.`, `No saved chats yet — start a conversation and it will be saved automatically.`
+ - State-language survivors (PRESENT, proving no over-deletion): `Saved chats
`, `Sign in to view your saved chats`, `Saved conversations are admin-only`.
+2. `tests/e2e/test_history_copy.py` (NEW — one phase, one file, run in isolation, DB up; shared conftest server, default env):
+ - Setup (house patterns from `tests/e2e/test_chat_history.py`): `_reset_db` with the TRUNCATE extended to include `saved_chats` (so the table is genuinely empty), `login(page, app_url, next="/history.html")` from `tests/e2e/auth_helpers.py`.
+ - Admin view of an empty History tab:
+ - the anonymous gate is hidden (`#history-gate` hidden — the login worked);
+ - `#history-empty-row` is revealed (visible) and its textContent == the locked (A3) empty-row string;
+ - the `.page-sub` innerText == the locked (A3) page-sub string (whitespace-normalized compare);
+ - `document.querySelector('meta[name="description"]').content` == the locked (A3) meta string;
+ - the no-button contract: `document.body.innerText` matches NEITHER `/press(ed)?\s+save/i` NOR `/save\s+button/i` — while the `` still reads "Saved chats" (asserted present, so the scan cannot pass by deleting the heading);
+ - no `#save-chat-btn` anywhere (phase 55 A2 — the control is gone from the app, not just the copy).
+ - Determinism: all assertions are settled-state (static HTML + the one 0-row fetch); Playwright `expect` retries ride out the table load.
+3. Regression E2E (each in isolation, DB up): `test_chat_history.py` (the row actions + restore flow on this page), `test_stale_saved_chats.py` (the stale pills + regenerate flow).
+4. One atomic commit (`.agent/` tracked — AGENTS.md rule 8):
+ ```bash
+ git add .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(web): history tab copy — every chat saves automatically, there is no Save button"
+ ```
+ Commit body: the verification read-through outcome from task 01 (the three replacements; h1/gate/history.js untouched) — one line each.
+
+## Testing & Quality
+- `uv run pytest` green (unit + integration); coverage TOTAL **>90%** (validate.sh gate — no `app/` change).
+- `uv run pytest tests/e2e/test_history_copy.py -v --no-cov` green in isolation (DB up) — run it after the unit pins pass.
+- `uv run ruff check . && uv run pyright` clean (the new test files included).
+
+## Completion Criteria
+- [ ] `tests/unit/test_history_copy.py` + `tests/e2e/test_history_copy.py` exist and are green (E2E in isolation, DB up).
+- [ ] The two regression E2E suites are green in isolation.
+- [ ] Full suite green, coverage >90%, lint + types clean.
+- [ ] Exactly one new commit on `main`, staging `.agent/ frontend/ tests/` only, `--no-gpg-sign`.
+- [ ] Phase dir moved to `.agent/phases/complete/66_history_auto_save_copy/` (`.agent/` stays tracked and committed).