phase: 100_page_width_consistency
Build and Push Containers / build-and-push-app (push) Successful in 4m3s
Build and Push Containers / build-and-push-db (push) Successful in 18s

**Phase 100 — final verification pass: all green.**

- Verified the shipped CSS contract directly: `--chat-column: 72rem` in `:root`; 0 literal `max-width: 46rem`; no `@media (min-width: 1500px)` block; exactly 4 token-capped reading columns; tuning/theme/doc-edit shells cap-free, structurally `.sources-shell`; `mock_llm.py` diff is comment-only.
- Defect found & fixed (phase-93 suite): `test_theme_semantic_completion.py::test_reset_removes_tag_byte_identical` raced theme.js's post-PUT refetch — it asserted the `#bor-theme` tag was gone right after the result line, but tag removal lands in the reconcile after the re-fetch (failed ~1 in 5 runs after `test_chat_rag`, reproduced). Fixed with an auto-waiting `expect(...).to_have_count(0)` (settled-state wait, the file's existing house pattern). 6/6 clean on the repro loop after.
- Tests/lint/coverage: `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, **99%** on `app/`; `uv run ruff check .` + `uv run pyright` → 0 errors.
- E2E in isolation (all passed): `test_wide_desktop_column.py` 3 (the phase suite — chat==tuning==theme==RAG ±4px at 1280 & 1920, ≈1152px; shared ≈1152px; standalone doc ≈1112px; modal unchanged ≈1100px; 360px overflow-free), `test_ui_customization` 4, `test_admin_theme_tab` 5, `test_document_viewer` 7, `test_save_share_ux` 5, `test_sticky_navbar` 3, `test_markdown_tables` 6, `test_responsive_polish` 7, `test_chat_rag` 3, `test_theme_semantic_completion` 8.
- Completion criteria: ① measured 72rem everywhere (≥~1200px, ±4px) + full-width below + 360px clean — **PASS** (E2E); ② zero 46rem rules / no 1500px block / four token selectors — **PASS** (grep + unit pins); ③ B4 byte-identical no-op + mobile squeeze — **PASS** (theme + responsive suites green); ④ full suite / coverage / lint — **PASS**; ⑤ atomic commit — left to the harness per executor protocol (all changes in the working tree, uncommitted).
- Deviations: none from phase decisions; only change in this pass is the race fix above (test-only, behavior unchanged).
- Next pending phase: `98_sync_summary_visibility` (numeric order in `todo/`; also pending: 99, 101–105).
This commit is contained in:
2026-09-12 13:37:19 -04:00
parent a2ca2f905f
commit 58e9d94cff
35 changed files with 1295 additions and 428 deletions
+71 -78
View File
@@ -45,10 +45,13 @@
--header-h: 64px;
/* Reading column: 46rem base (PLAN §7 lineage); 2x on wide desktops
(owner instruction 2026-08-31, TODO L5 / D2 — chat + shared +
document view). */
--chat-column: 46rem;
/* Reading column: 72rem — EQUAL to the .container's 72rem cap, so
the reading columns (chat, shared, the document viewer's .doc-md
+ its summary panel) ride the same width as every other page
(owner instruction 2026-09-12: "match the width of the RAG page
for all other pages" — supersedes the 2026-08-31 instruction, and
the wide-desktop doubling with it). */
--chat-column: 72rem;
}
* { box-sizing: border-box; }
@@ -389,8 +392,11 @@ body::before {
/* Chat is a vertical conversation: a centered, capped column is the
correct layout here (PLAN §UI/UX). The surrounding frame keeps it
from collapsing into a hairline on wide screens. The cap is the
--chat-column token: 46rem base, 2x (92rem) at >=1500px wide
desktops (owner instruction 2026-08-31, TODO.md L5 / D2). */
--chat-column token: 72rem — equal to the .container's cap, so the
chat column reads at the RAG page's full container width at every
viewport (owner instruction 2026-09-12: "match the width of the
RAG page for all other pages" — the wide-desktop doubling was
retired with it). */
.chat-shell {
max-width: var(--chat-column);
margin-inline: auto;
@@ -409,8 +415,8 @@ body::before {
flips this to a vertical stack (flex-direction: column +
align-items: stretch — full-width pills, New chat above Share; the
existing ≤640px pill rules apply to the stacked pills unchanged).
The reading-column contract is untouched (--chat-column: 46rem base,
92rem at >=1500px — PLAN §7 lineage).
The reading-column contract is untouched (--chat-column: 72rem —
the .container width at every viewport).
Phase 65 (task 03, 2026-09-01, `TODO.md` L3, owner-locked A2): the
row is the TOP member of the pinned .chat-bottom cluster (task
@@ -504,8 +510,9 @@ body::before {
/* GFM pipe tables (phase 44, 2026-08-27, TODO.md L6): the shared
renderer wraps every table in .md-table-wrap — the horizontal
scroller, so a wide table scrolls inside the bubble instead of
breaking the reading column (--chat-column: 46rem base, 92rem at
>=1500px) — around a semantic <table class="md-table">
breaking the reading column (--chat-column: 72rem — the
.container width at every viewport) — around a semantic
<table class="md-table">
(escape-first cells; alignment colons render left, owner decision).
Phase-08 tokens only: --line hairline borders and the thead tinted
from the plain surface family — --ink on --surface is 14.5:1 (PLAN
@@ -603,8 +610,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
--surface ≈10.4:1 (11.6:1 on the page bg), and --ink on --brand-soft
in the path `code` ≈11.5:1 — all comfortably AA in the (single dark)
theme. Inline rows only: appending lines never shifts the chat
column (46rem base; 92rem at >=1500px — no new container), and the
rows are not interactive — no focus targets. */
column (72rem — the .container width at every viewport; no new
container), and the rows are not interactive — no focus targets. */
.tool-calls {
display: flex;
flex-direction: column;
@@ -960,15 +967,16 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Global tuning page (phase 27) ---------- */
/* /tuning.html: create / edit / delete steering notes without a chat
conversation. Same width discipline as the chat column — a centered,
capped column on the 72rem frame; the form and the note list span its
FULL width (no skinny lists). Every interactive target is >=44px;
text pairs reuse the Phase-08 AA palette (dark ink on brand 5.2:1,
brand-ink/brand-soft 6.9:1, ok 10.6:1, err 9.1:1, ink-soft >=6.9:1).
No filter: blur, no CDN, system font stack. */
conversation. Full-container width (owner instruction 2026-09-12:
"match the width of the RAG page for all other pages" — the
phase-27 capped-column discipline is superseded): the shell rides
the .container's 72rem frame like the RAG page, and the form and
the note list span its FULL width (no skinny lists). Every
interactive target is >=44px; text pairs reuse the Phase-08 AA
palette (dark ink on brand 5.2:1, brand-ink/brand-soft 6.9:1,
ok 10.6:1, err 9.1:1, ink-soft >=6.9:1). No filter: blur, no CDN,
system font stack. */
.tuning-shell {
max-width: 46rem;
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 1.25rem;
@@ -1241,8 +1249,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Composer ---------- */
/* Phase 52 (2026-08-30, TODO.md L3): the composer is PINNED to the
viewport bottom. The page scrolls at the document level and
`.chat-shell` (the centered reading column — 46rem base, 92rem at
>=1500px) is the composer's sticky
`.chat-shell` (the centered reading column — 72rem, the
.container width at every viewport) is the composer's sticky
containing block, so the box sticks to the bottom edge of the
viewport at every scroll position and settles back into its normal
flow position (above the footer) once the document bottom is
@@ -2265,8 +2273,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
a fixed full-viewport dim backdrop + a centered panel (z-index
1000, above the sticky header (20) + skip-link (100); NO blur —
the phase-08 no-blur perf anchor), scaled to a compact dialog:
the 46rem chat-column width or the viewport, whichever is
narrower. Phase-08 tokens only; system fonts; no CDN.
the chat-column width (the --chat-column token — 72rem, the
.container width) or the viewport, whichever is narrower.
Phase-08 tokens only; system fonts; no CDN.
AA pairs: title/copy are --ink on --surface (13.8:1); the source
value is --ink on --bg (16.7:1); the error line is the err pair
@@ -2302,7 +2311,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
position: relative;
z-index: 1;
margin: auto;
width: min(46rem, calc(100vw - 2rem));
width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
@@ -2405,7 +2414,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
#remove-confirm-dialog overlay contract (phase 69): a fixed
full-viewport dim backdrop + a centered panel (z-index 1000, above
the sticky header (20) + skip-link (100); NO blur — the phase-08
no-blur perf anchor), scaled to the 46rem chat-column width or the
no-blur perf anchor), scaled to the chat-column width (the
--chat-column token — 72rem, the .container width) or the
viewport, whichever is narrower. The box: a VISIBLE block label
(WCAG — never aria-label-only) over a mono textarea (the box is a
data entry, not prose — the var(--mono) stack); the error line is
@@ -2442,7 +2452,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
position: relative;
z-index: 1;
margin: auto;
width: min(46rem, calc(100vw - 2rem));
width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
@@ -3168,8 +3178,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Theme view (phase 91, tasks 04 + 05) ----------
The shell's seventh view (#view-theme): the admin palette + branding
editor (task 05). The centered 46rem column (the .tuning-shell
language — this is a form view, the tuning-view width pattern), the
editor (task 05). The full 72rem container width (owner
instruction 2026-09-12 — the phase-91 form-column cap is
superseded), the
form card (the #tune-form language: surface fill, --line hairline,
radius, shadow), the fieldset groups (Branding / Palette) with the
.theme-note sub-copy, the 3-column (desktop) / 1-column (mobile)
@@ -3184,8 +3195,6 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
tab (B3). Every pair reuses the Phase-08 AA palette; :focus-visible
via the global 3px outline rule. No CDN, system fonts. */
.theme-shell {
max-width: 46rem;
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 1.25rem;
@@ -3347,12 +3356,13 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Shared page (phase 51, task 03) ----------
/shared/<token>: the anonymous read-only conversation (owner-locked
2026-08-29, TODO.md L6). The shell maps to the PLAN §7 centered
chat column (--chat-column: 46rem base, 92rem at >=1500px) — the
conversation reads exactly like the chat
page (the .msg/.bubble/.thinking/.tool-calls/.msg-meta rules apply
unchanged) with NO composer, so the column contract holds for a
guest. Zero interactive controls (owner-locked): the chips are
2026-08-29, TODO.md L6). The shared page reads exactly like the
chat page — both at the 72rem container width (--chat-column, now
equal to the .container cap — owner instruction 2026-09-12; the
wide-desktop doubling was retired) — and the .msg/.bubble/.thinking/
.tool-calls/.msg-meta rules apply unchanged, with NO composer, so
the column contract holds for a guest. Zero interactive controls
(owner-locked): the chips are
plain text, so the pill families' pointer treatments are switched
off IN THIS SCOPE ONLY — the chat page's interactive chips keep
their styles untouched. Every pair reuses the Phase-08 AA palette
@@ -3361,8 +3371,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
fonts. */
.shared-shell {
width: 100%;
max-width: var(--chat-column); /* the PLAN §7 centered chat column
(46rem base; 92rem at >=1500px — phase 58) */
max-width: var(--chat-column); /* the centered chat column — 72rem,
the .container width at every viewport */
margin-inline: auto;
display: flex;
flex-direction: column;
@@ -3525,9 +3535,10 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
#doc-content { display: flex; flex-direction: column; }
.doc-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); }
/* Markdown: the centered reading column — 46rem base, 2x (92rem) at
>=1500px (PLAN §7.1 lineage; owner instruction 2026-08-31, TODO.md
L5 / D2). */
/* Markdown: the centered reading column — 72rem, the .container width
at every viewport (owner instruction 2026-09-12: "match the width
of the RAG page for all other pages" — the wide-desktop doubling
was retired). */
.doc-md {
width: 100%;
max-width: var(--chat-column);
@@ -3576,8 +3587,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
overflow-wrap: anywhere;
}
/* md/markdown: the panel matches the .doc-md centered reading column
(46rem base; 92rem at >=1500px — the same --chat-column token) —
it is the column's label. Raw formats stay full width (the
(72rem — the same --chat-column token, the .container width at
every viewport) — it is the column's label. Raw formats stay full width (the
.doc-raw default above), matching the full-width pre; in engines
without :has() the panel degrades to that full-width default. */
.doc-summary:has(+ .doc-md) {
@@ -3743,8 +3754,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Document modal (phase 26) ----------
"New documents should open in an almost-fullscreen modal, not in a new
page" (TODO.md L4). The overlay reuses the viewer page's .doc-meta
badge classes, the .doc-md reading column (46rem base; 92rem at
>=1500px), and the .doc-raw
badge classes, the .doc-md reading column (72rem — the .container
width; inside the modal the 1100px panel stays its effective
ceiling), and the .doc-raw
pre — this block only adds the chrome (backdrop, panel, header,
actions, scroll container). Phase-08 tokens only; NO blur (the
phase-08 no-blur perf anchor); no new assets; system fonts.
@@ -3886,9 +3898,10 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
}
/* The scroll container: vertical scroll lives HERE, never the viewport.
.doc-md keeps its centered reading column inside (46rem base;
92rem at >=1500px, capped there by the 1100px panel); .doc-raw keeps
its own overflow-x. tabindex="-1" in the markup is the JS focus target. */
.doc-md keeps its centered reading column inside (72rem — wider
than the 1100px panel's inner width, which stays its effective
ceiling); .doc-raw keeps its own overflow-x. tabindex="-1" in the
markup is the JS focus target. */
.doc-modal-content {
flex: 1;
min-height: 0;
@@ -3906,16 +3919,14 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/doc-edit.html: the admin-gated edit screen for a doc draft (title,
in-repo path, markdown body) — a FLOW page, not one of the app's
pages, so the header is SLIM (brand + "← Back to chat" only). The
46rem base column is HARD-CODED: a form column, not a reading
column — it does not ride --chat-column, so phase 58's wide-desktop
doubling never stretches the form. The .sources-gate gate is reused
verbatim (phases 16/35/50). Every pair reuses the Phase-08 AA
palette; touch targets >=44px; :focus-visible via the global 3px
outline rule. No CDN, system fonts. */
shell rides the full 72rem container like every other page (owner
instruction 2026-09-12 — the phase-59 form-column cap is
superseded). The .sources-gate gate is reused verbatim
(phases 16/35/50). Every pair reuses the Phase-08 AA palette;
touch targets >=44px; :focus-visible via the global 3px outline
rule. No CDN, system fonts. */
.doc-edit-shell {
width: 100%;
max-width: 46rem; /* the 46rem base column (hard-coded — see above) */
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 1.25rem;
@@ -4385,9 +4396,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
.theme-reset { width: 100%; }
/* Phase 51: the shared page squeezes like the chat column — the
title and the note step down (the empty-state-title family); the
shell keeps its base 46rem column (the >=1500px 92rem override
never applies here — it is already the narrowest box on the
page) and .msg-body's 92% override above applies. */
shell is full-width here (the 72rem cap never binds on a phone —
the container is already 100%) and .msg-body's 92% override
above applies. */
#shared-title { font-size: 1.35rem; }
.shared-note { font-size: 0.88rem; }
.footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; }
@@ -4418,24 +4429,6 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
}
}
/* Phase 58: wide desktops (viewport >=1500px) read the 2x column —
--chat-column doubles to 92rem for the four reading shells (chat,
shared, the document viewer's .doc-md and its summary panel). The
chat and shared shells ARE their .container (the token max-width
overrides the .container's 72rem cap — same specificity, later in
the file), but the document page's .container.doc-shell WRAPS the
column, so the 72rem cap would bind first and pin .doc-md at
~1112px: the wide block lifts the shell's cap to the column plus
the container's two 1.25rem gutters, letting .doc-md's own 92rem
cap bind (1472px at the 16px root). Everything below 1500px
renders exactly as before, and .tuning-shell (a form, not a
reading surface) keeps its hard-coded 46rem at every width
(owner instruction 2026-08-31, TODO.md L5 / D2). */
@media (min-width: 1500px) {
:root { --chat-column: 92rem; }
.doc-shell { max-width: calc(var(--chat-column) + 2 * 1.25rem); }
}
/* Phase 46: prefers-reduced-motion stills the mobile menu — no
180ms slide+fade; open/close snaps (the visibility/opacity flip
applies instantly) and stays correct. BOTH states are named: the