diff --git a/frontend/assets/sources.js b/frontend/assets/sources.js
index 028f9f3..ce8b809 100644
--- a/frontend/assets/sources.js
+++ b/frontend/assets/sources.js
@@ -30,9 +30,11 @@ import { fetchIsAdmin, initSharedHeader } from "./header.js";
*
* The §7.4 never-stale lifecycle: idle → click → POST /api/sync
* → running (2 s poll of GET /api/sync/status) → success | failed.
- * Admin-only: the button is hidden for anonymous users (initSharedHeader
- * hides it). A failed run opens an error modal (same as the former
- * header.js module — recreated here since the navbar button is gone).
+ * Admin-only: the button SHIPS hidden and the page boot below reveals it
+ * for the admin on the SAME cached whoami initSharedHeader() used (no
+ * extra fetch); the 403 branches stay as defense in depth. A failed run
+ * opens an error modal (same as the former header.js module — recreated
+ * here since the navbar button is gone).
*
* Elements: #sync-btn (the button), #sync-label (the text),
* #sync-icon (the spinner icon), #sync-result (aria-live result
@@ -165,7 +167,7 @@ function closeSyncModal() {
function applySyncSuccess(status) {
const time = fmtSyncTime(status.finished_at);
settleSyncButton(time ? `Synced ${time}` : "Synced");
- if (syncResult) syncResult.textContent = fmtSyncResult(status);
+ if (syncResult) syncResult.textContent = fmtSyncResult(status.detail);
hideSyncError();
emitSyncStatus(status);
// Refresh the catalog live — the KB just changed.
@@ -411,7 +413,8 @@ function showEmpty() {
(async () => {
- await initSharedHeader(); // phase 19: Sign in/out + Sources link in the shared bar
+ const admin = await initSharedHeader(); // phase 19: shared bar (cached whoami)
+ if (syncBtn) syncBtn.hidden = !admin; // admin-only: ship-hidden, revealed on the same whoami
if (!(await isAdmin())) {
// Anonymous: gate in, catalog out, and no /api/docs request at all.
if (statCards) statCards.hidden = true;
diff --git a/tests/e2e/test_background_animation.py b/tests/e2e/test_background_animation.py
index a4be127..0e8c73c 100644
--- a/tests/e2e/test_background_animation.py
+++ b/tests/e2e/test_background_animation.py
@@ -47,7 +47,7 @@ Test → story mapping (Playwright Mapping Rule):
::before``/``::after`` spots): ``position: fixed``, ``z-index: -1``,
``pointer-events: none``, ``inset: 0`` (UI Structure Check: behind
content, click-through, full-viewport); the page canvas stays on
- ```` (``rgb(10, 14, 23)`` = ``var(--bg)``) and ``
``
+ ```` (``rgb(15, 10, 10)`` = ``var(--bg)``) and ````
stays transparent (``rgba(0, 0, 0, 0)``) — the no-occlusion
contract.
5. ``test_no_horizontal_overflow_with_layers`` — at a 360px viewport
@@ -71,7 +71,7 @@ from playwright.sync_api import Browser, Page
GRID_OLD = "bg-grid-drift" # deleted in phase 25 — must not exist anywhere
GLOWS = ("bg-glow-a", "bg-glow-b", "bg-glow-c") # the three phase-25 spot fades
-PAGE_BG = "rgb(10, 14, 23)" # var(--bg) — the canvas (phase-08 palette)
+PAGE_BG = "rgb(15, 10, 10)" # var(--bg) — the canvas (dark-red rebrand #0f0a0a)
# Computed styles of all four pseudo-layers + the html/body background
# contract (single evaluate — one round-trip per test).
diff --git a/tests/e2e/test_background_no_motion.py b/tests/e2e/test_background_no_motion.py
index 22fce0c..e73f504 100644
--- a/tests/e2e/test_background_no_motion.py
+++ b/tests/e2e/test_background_no_motion.py
@@ -67,7 +67,7 @@ Test → story mapping (Playwright Mapping Rule):
6. ``test_background_layers_contracts`` — all four pseudo-layers:
``position: fixed``, ``z-index: -1``, ``pointer-events: none``,
top/right/bottom/left all ``0px``; the ``documentElement`` computed
- background is ``rgb(10, 14, 23)`` (``var(--bg)`` — the canvas stays
+ background is ``rgb(15, 10, 10)`` (``var(--bg)`` — the canvas stays
on ``html``); ``document.body`` computed background is
``rgba(0, 0, 0, 0)`` (no occlusion).
7. ``test_reduced_motion_stills_all_layers`` —
@@ -99,7 +99,7 @@ GLOW_B = "bg-glow-b" # html::before — phase-08 cyan spot, 34s, -12s delay
GLOW_C = "bg-glow-c" # html::after — third indigo spot, 42s, -23s delay
GLOWS = (GLOW_A, GLOW_B, GLOW_C) # the three spot keyframe names
EASE_IN_OUT = "ease-in-out" # computed animationTimingFunction for ease-in-out
-PAGE_BG = "rgb(10, 14, 23)" # var(--bg) — the canvas (phase-08 palette)
+PAGE_BG = "rgb(15, 10, 10)" # var(--bg) — the canvas (dark-red rebrand #0f0a0a)
# Computed styles of all four pseudo-layers + the html/body background
# contract (single evaluate — one round-trip per test).
diff --git a/tests/e2e/test_chat_persistence.py b/tests/e2e/test_chat_persistence.py
index f50c731..7b6a74a 100644
--- a/tests/e2e/test_chat_persistence.py
+++ b/tests/e2e/test_chat_persistence.py
@@ -272,13 +272,13 @@ def test_persists_across_page_navigation(
_ask_deflected(page, OFF_TOPIC) # mixed conversation: grounded + deflected
# A trip to Sources (phase 16: the catalog is admin-only — the trip
- # starts with a real form login). Phase 19 (owner permission
- # 2026-08-23): the New chat control is part of the SHARED bar, so it
- # is present on the sources page too — only clicking it would clear
- # the conversation (pinned in tests/e2e/test_shared_header.py).
+ # starts with a real form login). The New chat button is chat-page
+ # only (owner rework 2026-08-28 — it left the shared bar), so the
+ # sources page carries none of it (pinned in
+ # tests/e2e/test_shared_header.py).
login(page, app_url, next="/sources.html")
expect(page.locator("#docs-tbody tr").first).to_be_visible(timeout=15_000)
- expect(page.locator("#new-chat-btn")).to_be_visible()
+ expect(page.locator("#new-chat-btn")).to_have_count(0)
# Back to the chat: the conversation is exactly as left — both turns,
# the source chip, and the amber deflected bubble with its chips.
diff --git a/tests/e2e/test_dark_tech_theme.py b/tests/e2e/test_dark_tech_theme.py
index dd54d3d..b6fcb77 100644
--- a/tests/e2e/test_dark_tech_theme.py
+++ b/tests/e2e/test_dark_tech_theme.py
@@ -173,13 +173,15 @@ def test_dark_palette_and_contrast(
>= 4.5:1 from live computed styles (not eyeballed)."""
for path in ("/", "/sources.html"):
page.goto(f"{app_url}{path}")
- # The visible page background is the canvas (rgb(10, 14, 23)
- # = #0a0e17). itself stays transparent so the z-index:-1
- # grid/glow layers (test_animated_background) are not painted over.
+ # The visible page background is the canvas (rgb(15, 10, 10)
+ # = #0f0a0a — the 2026-08-28 dark-red rebrand; it was the phase-08
+ # #0a0e17 canvas). itself stays transparent so the
+ # z-index:-1 grid/glow layers (test_animated_background) are not
+ # painted over.
bg = page.evaluate(
"() => getComputedStyle(document.documentElement).backgroundColor"
)
- assert bg == "rgb(10, 14, 23)", f"expected the dark page bg on {path}, got {bg}"
+ assert bg == "rgb(15, 10, 10)", f"expected the dark page bg on {path}, got {bg}"
body_bg = page.evaluate("() => getComputedStyle(document.body).backgroundColor")
assert body_bg == "rgba(0, 0, 0, 0)", (
f"{path}: body must stay transparent (the background layers need to show)"
diff --git a/tests/e2e/test_document_viewer.py b/tests/e2e/test_document_viewer.py
index c08e481..d5cd8f7 100644
--- a/tests/e2e/test_document_viewer.py
+++ b/tests/e2e/test_document_viewer.py
@@ -370,7 +370,7 @@ def test_standalone_page_still_works(
page.goto(f"{app_url}/document.html?source=docs&path=homelab%2Fkubernetes.md")
expect(page.locator("#doc-content .doc-md")).not_to_be_empty()
bg = page.evaluate("() => getComputedStyle(document.documentElement).backgroundColor")
- assert bg == "rgb(10, 14, 23)"
+ assert bg == "rgb(15, 10, 10)", "the dark-red rebrand canvas (#0f0a0a)"
refs = page.evaluate(
"""() => [...document.querySelectorAll("script[src], link[href]")]
@@ -409,14 +409,15 @@ def test_modal_theme_and_no_cdn(
expect(page.locator("#doc-modal-title")).to_have_text("Kubernetes Homelab Cluster")
expect(page.locator("#doc-modal-content .doc-md")).not_to_be_empty()
- # Dark theme (phase 08): the page background is untouched, and the
- # modal panel sits on the Phase-08 surface colour (#121a2e).
+ # Dark theme (phase 08, dark-red rebrand 2026-08-28): the page
+ # background is untouched, and the modal panel sits on the --surface
+ # colour (#1a0f0f).
bg = page.evaluate("() => getComputedStyle(document.documentElement).backgroundColor")
- assert bg == "rgb(10, 14, 23)"
+ assert bg == "rgb(15, 10, 10)"
surface = page.evaluate(
"() => getComputedStyle(document.querySelector('.doc-modal-panel')).backgroundColor"
)
- assert surface == "rgb(18, 26, 46)", f"panel not on the Phase-08 surface: {surface}"
+ assert surface == "rgb(26, 15, 15)", f"panel not on the --surface colour: {surface}"
# No-CDN: every script/link reference on the chat page (the touched
# page) is same-origin or a data: URI — the modal adds no assets.
diff --git a/tests/e2e/test_global_tuning.py b/tests/e2e/test_global_tuning.py
index e9f4619..10cc1bc 100644
--- a/tests/e2e/test_global_tuning.py
+++ b/tests/e2e/test_global_tuning.py
@@ -416,12 +416,13 @@ def test_tuning_page_a11y_and_no_cdn(
assert announcer.get_attribute("role") == "status"
assert announcer.get_attribute("aria-live") == "polite"
- # ≥44px touch targets on every interactive control.
+ # ≥44px touch targets on every interactive control (the New chat
+ # button is chat-page only — owner rework 2026-08-28 — so it is not
+ # a tuning-page target).
for selector in (
"#tune-save",
"#tune-note",
"#nav-tuning",
- ".new-chat-btn",
"#sign-out-btn",
".tuning-edit",
".tuning-delete",
@@ -447,7 +448,7 @@ def test_tuning_page_a11y_and_no_cdn(
# Dark theme: the page canvas is the phase-08 dark bg (body stays
# transparent for the background layers).
bg = page.evaluate("() => getComputedStyle(document.documentElement).backgroundColor")
- assert bg == "rgb(10, 14, 23)", f"expected the dark page bg, got {bg}"
+ assert bg == "rgb(15, 10, 10)", f"expected the dark page bg (rebrand #0f0a0a), got {bg}"
# Contrast: the live text/background pairs compute ≥ 4.5:1 (AA).
pairs = page.evaluate(
diff --git a/tests/e2e/test_honest_deflection.py b/tests/e2e/test_honest_deflection.py
index 991a069..4a778ea 100644
--- a/tests/e2e/test_honest_deflection.py
+++ b/tests/e2e/test_honest_deflection.py
@@ -114,8 +114,9 @@ def test_off_topic_question_deflects_honestly(
expect(group.first).to_have_attribute("role", "list")
# Chip component contract: brand pill, ≥44px touch target.
chip_style = chips.first.evaluate("el => getComputedStyle(el)")
- assert chip_style["backgroundColor"] == "rgb(35, 43, 82)" # --brand-soft #232b52 (dark theme)
- assert chip_style["color"] == "rgb(165, 180, 252)" # --brand-ink #a5b4fc
+ # --brand-soft #2d0a0a / --brand-ink #fca5a5 (dark-red rebrand)
+ assert chip_style["backgroundColor"] == "rgb(45, 10, 10)"
+ assert chip_style["color"] == "rgb(252, 165, 165)"
box = chips.first.bounding_box()
assert box is not None and box["height"] >= 44
diff --git a/tests/e2e/test_sources_midstream_bug.py b/tests/e2e/test_sources_midstream_bug.py
index 8b75d0a..9b94a5d 100644
--- a/tests/e2e/test_sources_midstream_bug.py
+++ b/tests/e2e/test_sources_midstream_bug.py
@@ -328,8 +328,9 @@ def test_completed_turn_unaffected(
# ---------------------------------------------------------------------------
-# 4. The DELIBERATE clear is untouched: New Chat from the sources page
-# still clears the conversation (phase 14/19 contract)
+# 4. The DELIBERATE clear is untouched: New chat (chat-page only since
+# the owner rework 2026-08-28) still clears the conversation
+# (phase 14 contract)
# ---------------------------------------------------------------------------
@@ -341,18 +342,15 @@ def test_new_chat_still_clears_conversation(
_ask(page, QUESTION)
assert _stored(page) is not None
- # To the sources page (anonymous is fine — the shared bar carries the
- # New Chat control regardless of auth, phase 19).
- page.goto(app_url + "/sources.html")
+ # The New chat button is chat-page only (it left the shared bar at
+ # owner request, 2026-08-28) — the deliberate clear runs right here.
new_chat = page.locator("#new-chat-btn")
expect(new_chat).to_be_visible()
new_chat.click()
- # "New chat" on a non-chat page means "go to the chat, fresh": the
- # land page shows the empty state and the conversation key is GONE —
- # the deliberate clearChatStorage() is unaffected by the phase-20
- # pagehide save point.
- expect(page).to_have_url(app_url + "/")
+ # The chat lands on its empty state and the conversation key is
+ # GONE — the deliberate clear is unaffected by the phase-20 pagehide
+ # save point.
expect(page.locator("#empty-state")).to_be_visible()
expect(page.locator(".msg")).to_have_count(0)
assert _stored(page) is None, "New Chat must clear the localStorage key"
diff --git a/tests/e2e/test_suggestion_chips.py b/tests/e2e/test_suggestion_chips.py
index 1ed0b5f..f9a090b 100644
--- a/tests/e2e/test_suggestion_chips.py
+++ b/tests/e2e/test_suggestion_chips.py
@@ -108,8 +108,8 @@ def test_onboarding_chips_render(
expect(page.locator("#empty-state")).to_be_visible()
# Chip component contract: brand pill, >=44px touch target.
style = chips.first.evaluate("el => getComputedStyle(el)")
- assert style["backgroundColor"] == "rgb(35, 43, 82)" # --brand-soft #232b52 (dark theme)
- assert style["color"] == "rgb(165, 180, 252)" # --brand-ink #a5b4fc
+ assert style["backgroundColor"] == "rgb(45, 10, 10)" # --brand-soft #2d0a0a (dark-red rebrand)
+ assert style["color"] == "rgb(252, 165, 165)" # --brand-ink #fca5a5 (dark-red rebrand)
assert style["borderRadius"] == "999px"
box = chips.first.bounding_box()
assert box is not None and box["height"] >= 44
diff --git a/tests/e2e/test_summary_in_viewer.py b/tests/e2e/test_summary_in_viewer.py
index 8659d9e..1ab2304 100644
--- a/tests/e2e/test_summary_in_viewer.py
+++ b/tests/e2e/test_summary_in_viewer.py
@@ -181,15 +181,16 @@ def test_full_page_shows_summary_and_original_together(
assert order[0] == "doc-summary", f"panel not first: {order}"
assert "doc-raw" in order and order.index("doc-raw") > order.index("doc-summary")
- # Theme (phase 08): panel on the surface colour, title in brand-ink
- # (#a5b4fc on #121a2e ≈8.7:1 ≥4.5:1), and the panel matches the
+ # Theme (phase 08, dark-red rebrand 2026-08-28): panel on the
+ # surface colour, title in brand-ink (--brand-ink #fca5a5 on
+ # --surface #1a0f0f = 9.0:1 ≥4.5:1), and the panel matches the
# raw-content width — it does not break the content layout.
bg = panel.evaluate("el => getComputedStyle(el).backgroundColor")
- assert bg == "rgb(18, 26, 46)", f"panel not on the Phase-08 surface: {bg}"
+ assert bg == "rgb(26, 15, 15)", f"panel not on the --surface colour (rebrand #1a0f0f): {bg}"
title_color = panel.locator(".doc-summary-title").evaluate(
"el => getComputedStyle(el).color"
)
- assert title_color == "rgb(165, 180, 252)", f"title not brand-ink: {title_color}"
+ assert title_color == "rgb(252, 165, 165)", f"title not brand-ink: {title_color}"
panel_box = panel.bounding_box()
pre_box = pre.bounding_box()
assert panel_box is not None and pre_box is not None
diff --git a/tests/e2e/test_sync_model_down.py b/tests/e2e/test_sync_model_down.py
index 7c491f8..9e8cb49 100644
--- a/tests/e2e/test_sync_model_down.py
+++ b/tests/e2e/test_sync_model_down.py
@@ -376,10 +376,11 @@ def test_sync_error_surfaces_unaffected(
app) the ``#sync-error-banner`` still renders the error off
``bor:sync-status`` — the phase-32/34 contract the modal is
additive to."""
- # Trigger the failure on the chat page (the modal is module-owned —
- # it follows the button onto every page). The re-attach modal from
- # the prior test's last failure closes first (clean trigger state).
- login(page, dead_app_url, next="/")
+ # Trigger the failure on the Sources page (the button's home since
+ # the owner rework 2026-08-28; the modal is page-local there). The
+ # re-attach modal from the prior test's last failure closes first
+ # (clean trigger state).
+ login(page, dead_app_url)
_dismiss_reattached_modal(page)
_trigger_dead_failure(page, dead_app_url)
diff --git a/tests/unit/test_background_animation.py b/tests/unit/test_background_animation.py
index 6b8b76a..e3e3b97 100644
--- a/tests/unit/test_background_animation.py
+++ b/tests/unit/test_background_animation.py
@@ -14,12 +14,15 @@ The new design (styles.css, pure CSS, zero JS, no `filter` — A11):
- grid (body::before): a STATIC texture — the drift animation and its
keyframes are deleted (the 0.73px/s sub-pixel drift rasterizes as a
once-per-second down-right jitter);
- - three independent soft glow spots — body::after (phase-08 indigo,
- 26s), html::before (phase-08 cyan, 34s, -12s delay), html::after
- (third indigo, 42s, -23s delay) — each on its own SLOW opacity-only
- fade (the whole-layer breathe keyframes are deleted), so the total
- light fluxuates smoothly and irregularly; LCM(26, 34, 42) = 4641s,
- so the composite pattern never repeats within a viewing session.
+ - three independent soft glow spots — body::after (26s), html::before
+ (34s, -12s delay), html::after (42s, -23s delay) — each on its own
+ SLOW opacity-only fade (the whole-layer breathe keyframes are
+ deleted), so the total light fluxuates smoothly and irregularly;
+ LCM(26, 34, 42) = 4641s, so the composite pattern never repeats
+ within a viewing session. The 2026-08-28 rebrand recolored the
+ phase-08 indigo/cyan spots to the warm dark-red theme palette
+ (rose / orange / red) and the grid lines to the warm line tone —
+ structure (sizes, positions, alphas, periods) unchanged.
This file keeps the generic layer-plumbing pins (the no-occlusion
contract, fixed / z-index -1 / pointer-events none — now across all
@@ -153,17 +156,20 @@ def test_grid_is_static_no_drift() -> None:
def test_grid_cells_and_line_contrast() -> None:
- """44px cells with 1px lines at the phase-22 fixed 60% --line alpha
- (the static texture keeps the values that made the grid readable —
- see tests/unit/test_background_no_motion.py for the phase-25 story)."""
+ """44px cells with 1px lines at the phase-22 fixed 60% line alpha,
+ in the rebrand warm line tone (2026-08-28; the phase-22 indigo
+ value rgb(38 48 74 / 0.6) was recolored with the dark-red theme)
+ — the static texture keeps the values that made the grid readable
+ (see tests/unit/test_background_no_motion.py for the phase-25
+ story)."""
block = _grid_rule(_css())
assert "background-size: 44px 44px" in block
- line = "linear-gradient(to right, rgb(38 48 74 / 0.6) 1px, transparent 1px)"
- assert line in block, "grid must keep horizontal 1px lines at 60% --line"
+ line = "linear-gradient(to right, rgb(74 38 38 / 0.6) 1px, transparent 1px)"
+ assert line in block, "grid must keep horizontal 1px lines at 60% line alpha"
assert (
- "linear-gradient(to bottom, rgb(38 48 74 / 0.6) 1px, transparent 1px)"
+ "linear-gradient(to bottom, rgb(74 38 38 / 0.6) 1px, transparent 1px)"
in block
- ), "grid must keep vertical 1px lines at 60% --line"
+ ), "grid must keep vertical 1px lines at 60% line alpha"
assert "0.35" not in block, "the too-faint 35% line alpha must not return"
@@ -216,21 +222,23 @@ def test_glow_keyframes_are_opacity_only() -> None:
)
-def test_glow_spots_keep_phase08_colors_and_add_a_third() -> None:
- """The phase-08 spots keep their colors, radii and positions — the
- indigo top-left stays on body::after, the cyan bottom-right moves to
- html::before — and a third soft indigo spot (52rem at 14% 86%)
- joins on html::after. All spots fade to transparent at 62%."""
+def test_glow_spots_use_the_rebrand_warm_palette() -> None:
+ """The three spots keep their radii and positions from the phase-25
+ layout (rose top-left on body::after, orange bottom-right on
+ html::before, red bottom-left 52rem at 14% 86% on html::after) but
+ wear the 2026-08-28 rebrand palette (warm dark-red theme; the
+ phase-08 indigo/cyan values are gone). All spots fade to
+ transparent at 62%."""
assert (
- "radial-gradient(circle 56rem at 12% 8%, rgb(109 120 242 / 0.14), "
+ "radial-gradient(circle 56rem at 12% 8%, rgb(244 63 94 / 0.10), "
"transparent 62%)" in _glow_rule(_css())
)
assert (
- "radial-gradient(circle 60rem at 88% 92%, rgb(34 211 238 / 0.10), "
+ "radial-gradient(circle 60rem at 88% 92%, rgb(251 146 60 / 0.08), "
"transparent 62%)" in _rule_block(_css(), "html::before")
)
assert (
- "radial-gradient(circle 52rem at 14% 86%, rgb(109 120 242 / 0.09), "
+ "radial-gradient(circle 52rem at 14% 86%, rgb(239 68 68 / 0.08), "
"transparent 62%)" in _rule_block(_css(), "html::after")
)
diff --git a/tests/unit/test_background_no_motion.py b/tests/unit/test_background_no_motion.py
index 7ff5ef3..ece3dcc 100644
--- a/tests/unit/test_background_no_motion.py
+++ b/tests/unit/test_background_no_motion.py
@@ -34,13 +34,16 @@ from tests.unit.test_background_animation import _css, _css_no_comments, _rule_b
ALL_LAYERS = ("body::before", "body::after", "html::before", "html::after")
# (layer, keyframes name, duration shorthand, single-spot gradient)
+# The 2026-08-28 rebrand recolored the phase-08 indigo/cyan spots to the
+# warm dark-red theme palette (rose / orange / red) — structure
+# (radius, position, alpha, period, delay) is the phase-25 design.
GLOW_SPOTS = (
("body::after", "bg-glow-a", "animation: bg-glow-a 26s ease-in-out infinite",
- "radial-gradient(circle 56rem at 12% 8%, rgb(109 120 242 / 0.14), transparent 62%)"),
+ "radial-gradient(circle 56rem at 12% 8%, rgb(244 63 94 / 0.10), transparent 62%)"),
("html::before", "bg-glow-b", "animation: bg-glow-b 34s ease-in-out -12s infinite",
- "radial-gradient(circle 60rem at 88% 92%, rgb(34 211 238 / 0.10), transparent 62%)"),
+ "radial-gradient(circle 60rem at 88% 92%, rgb(251 146 60 / 0.08), transparent 62%)"),
("html::after", "bg-glow-c", "animation: bg-glow-c 42s ease-in-out -23s infinite",
- "radial-gradient(circle 52rem at 14% 86%, rgb(109 120 242 / 0.09), transparent 62%)"),
+ "radial-gradient(circle 52rem at 14% 86%, rgb(239 68 68 / 0.08), transparent 62%)"),
)
@@ -87,16 +90,17 @@ def test_drift_and_breathe_keyframes_are_deleted() -> None:
def test_grid_keeps_its_static_texture() -> None:
"""The owner rejected the grid's MOTION, not the grid: 44px cells,
- 1px lines at 60% --line alpha, and the widened radial mask (both the
- -webkit- and standard mask properties) stay."""
+ 1px lines at the fixed 60% line alpha (rebrand warm tone,
+ 2026-08-28), and the widened radial mask (both the -webkit- and
+ standard mask properties) stay."""
block = _rule_block(_css(), "body::before")
assert "background-size: 44px 44px" in block
assert (
- "linear-gradient(to right, rgb(38 48 74 / 0.6) 1px, transparent 1px)" in block
- ), "grid must keep horizontal 1px lines at 60% --line"
+ "linear-gradient(to right, rgb(74 38 38 / 0.6) 1px, transparent 1px)" in block
+ ), "grid must keep horizontal 1px lines at 60% line alpha"
assert (
- "linear-gradient(to bottom, rgb(38 48 74 / 0.6) 1px, transparent 1px)" in block
- ), "grid must keep vertical 1px lines at 60% --line"
+ "linear-gradient(to bottom, rgb(74 38 38 / 0.6) 1px, transparent 1px)" in block
+ ), "grid must keep vertical 1px lines at 60% line alpha"
mask = "radial-gradient(140% 110% at 50% 0%, black 40%, transparent 90%)"
assert f"-webkit-mask-image: {mask};" in block
assert f"mask-image: {mask};" in block
@@ -138,10 +142,11 @@ def test_glow_layers_declare_no_transform_or_position_animation() -> None:
def test_each_spot_runs_its_own_slow_opacity_fade() -> None:
- """body::after (phase-08 indigo, 26s), html::before (phase-08 cyan,
- 34s, -12s delay), html::after (third indigo, 42s, -23s delay) — each
- glow layer's background-image is EXACTLY the single radial gradient
- from the spec (color, radius, position, 62% transparent stop)."""
+ """body::after (rose, 26s), html::before (orange, 34s, -12s delay),
+ html::after (red, 42s, -23s delay) — the rebrand warm palette on
+ the phase-25 layout; each glow layer's background-image is EXACTLY
+ the single radial gradient from the spec (color, radius, position,
+ 62% transparent stop)."""
for sel, _name, animation, gradient in GLOW_SPOTS:
block = _rule_block(_css(), sel)
assert animation in block, f"{sel} must run {animation}"
diff --git a/tests/unit/test_chat_persistence.py b/tests/unit/test_chat_persistence.py
index 8c49809..6c1ee03 100644
--- a/tests/unit/test_chat_persistence.py
+++ b/tests/unit/test_chat_persistence.py
@@ -10,8 +10,10 @@ Pinned design (PLAN §7.4 note / phase 14):
* save points: user message on send, brain message on ``done``;
* every ``localStorage`` access wrapped in try/catch (failure-safe);
* size budget ~700k chars, oldest dropped first;
-* ``#new-chat-btn`` in the shared header bar (chat, sources, viewer —
- phase 19, owner permission 2026-08-23), ≥44px, ghost pill.
+* ``#new-chat-btn`` chat-page only — phase 19 shipped it in the shared
+ bar (chat, sources, viewer, owner permission 2026-08-23); it moved
+ from the navbar to index.html's .chat-shell at owner request
+ (2026-08-28). ≥44px, solid brand pill.
"""
from __future__ import annotations
@@ -22,7 +24,10 @@ FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
APP_JS = FRONTEND / "assets" / "app.js"
INDEX_HTML = FRONTEND / "index.html"
SOURCES_HTML = FRONTEND / "sources.html"
+GIT_SOURCES_HTML = FRONTEND / "git-sources.html"
DOCUMENT_HTML = FRONTEND / "document.html"
+LOGIN_HTML = FRONTEND / "login.html"
+TUNING_HTML = FRONTEND / "tuning.html"
STYLES_CSS = FRONTEND / "assets" / "styles.css"
@@ -151,53 +156,58 @@ def test_new_chat_clears_key_and_ui() -> None:
assert "removeItem(STORAGE_KEY)" in js
-def test_new_chat_button_in_the_shared_header_bar() -> None:
- """#new-chat-btn is a real type=button with an accessible name in the
- chat header (index.html). Phase 19 (owner permission 2026-08-23): it
- is part of the SHARED bar — it also appears in sources.html and the
- document viewer, where it means "go to the chat, fresh" (the
- clear-storage + navigate binding is pinned in test_shared_header.py)."""
- for html in (INDEX_HTML, SOURCES_HTML, DOCUMENT_HTML):
- text = html.read_text(encoding="utf-8")
- btn = re.search(r'