/* ========================================================================== Brain of Reese — design system (no CDN; system fonts only) Dark red theme (phase 08): emoji-free chrome, subtle animated pure-CSS background, WCAG 2.1 AA dark palette (every pair computed >= 4.5:1). ========================================================================== */ :root { /* Palette — all text/background pairs meet WCAG 2.1 AA (>= 4.5:1) */ --bg: #0f0a0a; /* page: ink on bg 16.7:1 */ --surface: #1a0f0f; /* ink on surface 13.8:1 */ --ink: #f0e6e6; --ink-soft: #b8a8a8; /* 5.1:1 on --surface */ --line: #2d1a1a; /* decorative 1px borders */ --grid-line: #4a2626; /* background grid texture — decorative, no contrast duty (like --line) */ --brand: #f43f5e; /* text on brand is DARK ink (--bg): 5.2:1 — never white on brand (3.7:1, fails) */ --brand-soft: #2d0a0a; --brand-ink: #fca5a5; /* 9.0:1 on --surface, 12.4:1 on --brand-soft */ /* Derived state colors (phase 92, task 02) — COMPUTED in :root from the identity palette via color-mix(): NOT stored, NOT tab-controlled, NOT in BUILTIN_COLORS (theming.py). Every state follows the theme; the built-in defaults reproduce the pre-phase-92 look, or are the deliberate legacy-indigo fixes (hovers, busy). */ --brand-hover: color-mix(in srgb, var(--brand) 86%, white); --brand-busy: color-mix(in srgb, var(--brand) 40%, white); --brand-stop: color-mix(in srgb, var(--brand) 75%, var(--bg)); --accent-bg: #2b2110; --accent-ink: #fbbf24; /* 9.5:1 on --accent-bg */ --accent-line: #f59e0b; /* 8.9:1 on --bg (deflection border) */ --ok-bg: #10241b; --ok-ink: #6ee7a8; /* 10.6:1 on --ok-bg */ --err-bg: #2d0a0a; --err-ink: #fca5a5; /* 9.3:1 on --err-bg */ --err-line: #ef4444; /* 4.6:1 on --err-bg (UI boundary, not text) */ /* Scrollbar tokens (phase 116, task 01) — the document modal's raw code block scrolls horizontally (locked A1: code never wraps), so its scrollbar must match the theme instead of the browser-native light bar. Like the derived state colors above, COMPUTED in :root from the identity palette via color-mix(): NOT stored, NOT tab-controlled, NOT in BUILTIN_COLORS (theming.py) — every saved theme repaints them. Muted thumb on a near-code-background track (the .doc-raw background is --surface); the thumb holds >= 3:1 non-text contrast against the track (WCAG 2.1 AA — 3.7:1 at the built-in palette). */ --scrollbar-thumb: color-mix(in srgb, var(--ink-soft) 60%, var(--surface)); --scrollbar-track: color-mix(in srgb, var(--surface) 75%, var(--bg)); --radius: 10px; --radius-sm: 6px; --shadow: 0 1px 2px rgb(0 0 0 / 0.30), 0 4px 16px rgb(0 0 0 / 0.35); --shadow-lg: 0 4px 10px rgb(0 0 0 / 0.40), 0 12px 32px rgb(0 0 0 / 0.50); --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --header-h: 64px; /* 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; } /* Phase 60 (owner confirmation 2026-08-31, TODO L3): body must NOT carry a fixed height — a sticky element's travel range is capped by its containing block, and `body { height: 100% }` pinned the box to one viewport, so the navbar un-pinned after ~1 viewport of scroll. `min-height: 100dvh` on body (below) is what stretches short pages. */ html { height: 100%; } /* The visible page background lives on (the canvas). must stay transparent and must NOT create a stacking context, or the z-index:-1 background layers below would be painted over. */ html { background: var(--bg); } body { margin: 0; font-family: var(--font); font-size: 16px; line-height: 1.55; color: var(--ink); background: transparent; position: relative; display: flex; flex-direction: column; min-height: 100dvh; } /* ---------- Static background (pure CSS, zero JS) ---------- Phase 78 (owner direction, TODO.md L4): the animated background was removed as too resource-intensive — the three opacity-fading glow spots (and their keyframes) are deleted. What remains is fully static: the 44px grid texture below — zero animation cost, zero JS, no filter/blur. */ /* Static grid texture: 44px cells, 1px lines at 60% --grid-line, masked with a widened radial fade (visible across most of the viewport, fading to the corners). Phase 25 (owner 2026-08-25): the grid drift is REMOVED — the 0.73px/s sub-pixel drift rasterizes as a once-per-second down-right jitter, and the owner wants no movement. The grid stays as a still texture. Phase 92 (task 02): the line color is the 9th identity variable --grid-line (the built-in at 60% alpha — the tab's "Grid lines" picker repaints this texture; it was a hardcoded literal before). */ body::before { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background-image: linear-gradient(to right, color-mix(in srgb, var(--grid-line) 60%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in srgb, var(--grid-line) 60%, transparent) 1px, transparent 1px); background-size: 44px 44px; -webkit-mask-image: radial-gradient(140% 110% at 50% 0%, black 40%, transparent 90%); mask-image: radial-gradient(140% 110% at 50% 0%, black 40%, transparent 90%); } .container { width: 100%; max-width: 72rem; margin-inline: auto; padding-inline: 1.25rem; } /* ---------- Accessibility helpers ---------- */ /* The `hidden` attribute must always win — some components set an explicit `display` (e.g. .kb-banner { display: flex }) which would otherwise override the UA stylesheet's `[hidden] { display: none }` and leave the element visible. */ [hidden] { display: none !important; } .visually-hidden { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } .skip-link { position: absolute; left: -9999px; top: 0; background: var(--brand); color: var(--bg); padding: 0.6rem 1rem; border-radius: 0 0 var(--radius-sm) 0; z-index: 100; } .skip-link:focus { left: 0; } :focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 4px; } ::selection { background: color-mix(in srgb, var(--brand) 45%, transparent); color: var(--ink); } /* ---------- Header ---------- */ .app-header { height: var(--header-h); background: var(--surface); position: sticky; top: 0; z-index: 20; /* Phase 12 (reworded phase 60): body is a flex column that stretches to at least one viewport (min-height: 100dvh) and grows with its content; without this the header shrinks (flex-shrink:1) to its content minimum on any page whose content overflows the viewport (e.g. Sources at ≤640px). */ flex-shrink: 0; } /* 2px brand-fade hairline under the sticky header (phase 08; shared by the app header and the document-viewer header, phase 10). Phase 92 (task 02): the pre-theme rose→orange→amber art direction retires — the 2px line now fades from the brand (55% → 30% → 5% of --brand) and follows the theme (deliberate default change). In the viewer's two-row header (phase 34) this lands at the BOTTOM edge of the whole header — row 1's own copy is suppressed there (see the .doc-header rules below). */ .app-header::after, .doc-header::after { content: ""; position: absolute; inset-inline: 0; bottom: -2px; height: 2px; pointer-events: none; background: linear-gradient( 90deg, color-mix(in srgb, var(--brand) 55%, transparent), color-mix(in srgb, var(--brand) 30%, transparent) 45%, color-mix(in srgb, var(--brand) 5%, transparent) 90% ); } /* margin-left:auto on the nav (not justify-content:space-between) so the phase-14 "New chat" pill clusters with the nav on the right while the two-child Sources header keeps the exact same look. */ .header-inner { height: 100%; display: flex; align-items: center; gap: 1rem; } .brand { display: inline-flex; align-items: center; gap: 0.55rem; font-size: 1.125rem; color: var(--ink); text-decoration: none; /* Phase 34 task 04 (visual pass): the bar must fit the FULL admin control set at every width — the wordmark is the designated squeeze target (min-width:0 lets flex shrink it; overflow:hidden clips it clean instead of letting it overlap the nav). */ min-width: 0; overflow: hidden; } /* Mono wordmark with letter-spacing — the "technical" touch (phase 08); ellipsizes as the squeeze target (phase 34 task 04). */ .brand-text { font-family: var(--mono); font-size: 0.95rem; letter-spacing: 0.08em; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .brand-mark { width: 22px; height: 22px; flex: 0 0 auto; display: block; } /* The wordmark's theming resolves HERE from :root — the markup carries no inline style attributes: this policy's style-src (phase 82 A1: 'self' + the theme tag's sha256, no 'unsafe-inline') blocks inline style attributes (hashes never cover them), so the identity vars land via this stylesheet instead (CSP-clean 'self'). The :nth-child keys follow the wordmark's fixed 3-shape markup (hexagon, dot, spokes) — identical in all five HTML files. */ .brand-mark path:nth-child(1) { fill: var(--surface); stroke: var(--brand); } .brand-mark circle { fill: var(--brand); } .brand-mark path:nth-child(3) { fill: none; stroke: var(--brand-ink); } .brand-text strong { color: var(--brand-ink); font-weight: 700; } .app-nav { display: flex; gap: 0.25rem; margin-left: auto; } .nav-link { padding: 0.5rem 0.9rem; border-radius: 999px; text-decoration: none; color: var(--ink-soft); font-weight: 600; font-size: 0.95rem; min-height: 44px; display: inline-flex; align-items: center; } .nav-link:hover { background: var(--brand-soft); color: var(--brand-ink); } .nav-link.is-active { background: var(--brand); color: var(--bg); } /* Phase 46 (owner permission 2026-08-27, TODO.md L9): the mobile hamburger button — desktop is byte-identical to before (the control is absent outside the ≤640px block, which re-displays it and turns the nav into the dropdown). :focus-visible inherits the global 3px outline rule; the 44px target + the rest of the look live in the ≤640px block below. */ .nav-toggle { display: none; } /* "New chat" reset (phase 14): ghost pill in the chat header, hover like a nav link. ink-soft on surface ≈6.9:1; hover pair brand-ink/brand-soft ≈6.9:1 — both WCAG AA. Icon-only below 640px (aria-label keeps the accessible name); ≥44px touch target at every width. */ .new-chat-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; min-height: 44px; padding: 0.5rem 0.9rem; border-radius: 999px; border: 0; background: var(--brand); color: var(--bg); font: inherit; font-weight: 700; font-size: 0.95rem; white-space: nowrap; cursor: pointer; } .new-chat-btn:hover { background: var(--brand-hover); color: var(--bg); } /* The plus mark is hidden on desktop (label carries the pill); it is the whole control below 640px. */ .new-chat-btn svg { width: 16px; height: 16px; display: none; } /* Phase 55 (owner-locked A2, 2026-08-31): the phase-50 Save-pill rules are GONE with the pill — every conversation auto-saves (app.js); the chat-shell actions are New chat + Share only. Phase 51 (owner-locked 2026-08-29, TODO.md L6): the "Share" pill — the EXACT visual family of .new-chat-btn (same declarations, so the chat-shell actions always read as a pair): solid brand pill, --bg text on --brand (5.2:1, WCAG AA), borderless, ≥44px target, hover lightens the brand fill, focus-visible via the global 3px rule. Shipped hidden in index.html — app.js reveals it for admin only (phase 16 absent-not-hidden); ≤640px overrides below mirror the New chat ones (label stays visible in .chat-shell, icon hidden there). */ .share-chat-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; min-height: 44px; padding: 0.5rem 0.9rem; border-radius: 999px; border: 0; background: var(--brand); color: var(--bg); font: inherit; font-weight: 700; font-size: 0.95rem; white-space: nowrap; cursor: pointer; } .share-chat-btn:hover { background: var(--brand-hover); color: var(--bg); } /* The link mark is hidden on desktop (the label carries the pill); it is the whole control below 640px (mirrored in the ≤640 block below). */ .share-chat-btn svg { width: 16px; height: 16px; display: none; } /* Phase 16: header auth controls (Sign in link / Sign out button) — the same ghost pill as New chat, so the bar keeps one visual language. ink-soft on surface ≈6.9:1; hover pair brand-ink/brand-soft ≈6.9:1. Icon-only below 640px (aria-labels/labels keep the accessible names); ≥44px touch target at every width. Exactly one is ever visible. */ .auth-link { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; min-height: 44px; padding: 0.5rem 0.9rem; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.95rem; white-space: nowrap; text-decoration: none; cursor: pointer; } .auth-link:hover { background: var(--brand-soft); color: var(--brand-ink); } .auth-link:disabled { opacity: 0.6; cursor: wait; } .auth-link svg { width: 16px; height: 16px; display: none; } /* Mobile-only sign-out / sign-in copies: hidden on desktop, revealed inside the hamburger dropdown on mobile (phase 46 UX revision). */ .sign-out-mobile, .sign-in-mobile { display: none; } /* ---------- Main frame ---------- */ /* Phase 52 (owner revision 2026-08-30): `flex: 1` is load-bearing, not cosmetic. Body is a `min-height: 100dvh` column flex box, so `main` grows to the full viewport height and `.chat-shell` grows to fill `main`. That full-height column is what makes the composer's sticky shift range tall enough to hold the box on screen — the pin works in BOTH directions only because of it (see `.composer` and `.messages`). No `overflow` here: the document must stay the scroll container. */ .app-main { flex: 1; display: flex; flex-direction: column; padding-block: 1.25rem; } /* Phase 76 (task 02): the shell's #view-chat wrapper sits between .app-main and .chat-shell — it must CONTINUE the full-height column chain (body's min-height: 100dvh flex → .app-main → #view-chat → .chat-shell, phase 52) or .chat-shell's flex:1 loses its flex parent and the column sizes to content: the composer's sticky pin (phases 43/55/65) then has no tall scroll range and the empty chat stops resting at the screen bottom. The folded document views (tuning/rag/git-sources) size to content in normal flow — they need no chain. */ #view-chat { flex: 1; display: flex; flex-direction: column; } /* 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: 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; display: flex; flex-direction: column; gap: 1rem; flex: 1; } /* Phase 55 (task 05, 2026-08-31, TODO.md L6, owner-locked A5): the chat-actions row — New chat + Share share ONE .chat-actions element (a normal .chat-shell column child). As a flex ITEM of the column it spans the column width, but as a flex ROW with align-items: center (not the column default stretch) each pill keeps its INTRINSIC content width — two pills side by side on desktop. The ≤640px block 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: 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 01/02) and hugs the column's RIGHT edge on desktop (justify-content: flex-end) — mirroring the right-aligned "Save as doc" action of every brain bubble above it, so the bottom-right of the chat reads as ONE aligned action column. The pills keep their intrinsic widths (align-items: center already beats the column default stretch; justify-content only shifts the group). The vertical row↔composer spacing is the .chat-bottom wrapper's 0.5rem gap (task 02). The ≤640px stack rule below is UNCHANGED — with stretched full-width pills the horizontal alignment is moot (the stacked pills already edge-to-edge the column). */ .chat-actions { display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 0.6rem; } /* Phase 52 (owner revision 2026-08-30): `flex-grow` is the other half of the pin. `position: sticky` only ever pulls a box UP toward the viewport bottom — it can never push a box DOWN to meet it — so on an empty/short chat the composer used to sit just under the empty state, mid-screen, with a dead band between it and the footer. Growing the message list absorbs that free space instead, so the composer's resting (in-flow) position already IS the bottom of the screen. Once the conversation is taller than the viewport there is no free space left to absorb, `flex-grow` does nothing, and the `.composer` sticky rule takes over. `flex-basis: auto` (not `1`) so the list keeps its content height when the page overflows; the phase-01 `min-height` floor stays; still no inner scroller (the document scrolls). */ .messages { flex: 1 1 auto; display: flex; flex-direction: column; gap: 0.9rem; min-height: 200px; } /* ---------- Messages ---------- */ .msg { display: flex; gap: 0.6rem; max-width: 100%; } .msg .avatar { flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--brand-soft); border: 1px solid var(--line); } /* Emoji-free inline SVG glyphs (phase 08), currentColor so the theme controls the stroke; sized to sit crisp at ~16-20px. */ .msg .avatar svg { width: 20px; height: 20px; display: block; } .msg.brain .avatar { color: var(--brand-ink); } .msg.user .avatar { color: var(--ink-soft); } .msg-body { max-width: 85%; /* min-width: 0 — as a flex item this overrides min-width:auto so a long unbroken token can wrap (overflow-wrap: anywhere) instead of widening the bubble past the column (AC7, phase 07). */ min-width: 0; display: flex; flex-direction: column; gap: 0.35rem; } .bubble { padding: 0.7rem 1rem; border-radius: var(--radius); background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow); overflow-wrap: anywhere; } .bubble p { margin: 0.2rem 0; } .bubble pre { background: var(--surface); color: var(--ink); padding: 0.7rem 0.9rem; border: 1px solid var(--line); border-radius: var(--radius-sm); overflow-x: auto; font-size: 0.85rem; font-family: var(--mono); } .bubble code { font-family: var(--mono); font-size: 0.88em; background: var(--brand-soft); padding: 0.08em 0.35em; border-radius: 5px; } .bubble pre code { background: none; padding: 0; } /* 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: 72rem — the .container width at every viewport) — around a semantic (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 §7.2), never the brand. The rules are unscoped on purpose: the same renderer serves the chat bubble, the thinking scratchpad, and the document viewer (.doc-md). width:100% stretches narrow tables to the column; min-width:max-content lets a WIDE table keep its natural width so the wrapper is the real scroller (phase 44 task 03: width:100% alone wrapped the wide table's cells and it never overflowed). Static content — no animation (nothing for prefers-reduced-motion to still). */ .md-table-wrap { overflow-x: auto; } .md-table { border-collapse: collapse; width: 100%; min-width: max-content; font-size: 0.9rem; } .md-table th, .md-table td { border: 1px solid var(--line); padding: 0.4rem 0.6rem; text-align: left; vertical-align: top; } .md-table thead th { background: var(--surface); color: var(--ink); } .msg.user { justify-content: flex-end; } .msg.user .msg-body { align-items: flex-end; } .msg.user .bubble { background: var(--brand); border-color: var(--brand); color: var(--bg); border-bottom-right-radius: 4px; } .msg.user .bubble code { background: color-mix(in srgb, var(--bg) 16%, transparent); } .msg.brain .bubble { border-bottom-left-radius: 4px; } .msg.brain.is-deflected .bubble { background: var(--accent-bg); border-color: var(--accent-line); } /* Collapsible "Thinking" block (phase 17): the model's reasoning streams open ABOVE the answer bubble, auto-collapses when the answer starts, and stays user-toggleable (native
/ — a real focusable control: >=44px target, :focus-visible via the global rule). Summary text is brand-ink on surface ≈8.7:1; the scratchpad body is ink-soft on surface ≈6.9:1 — both AA. */ details.thinking { background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--brand-soft); border-radius: var(--radius-sm); margin: 0 0 0.5rem; overflow: hidden; } details.thinking summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; min-height: 44px; color: var(--brand-ink); /* 8.7:1 on --surface */ font-size: 0.9rem; cursor: pointer; list-style: none; } details.thinking summary::-webkit-details-marker { display: none; } /* CSS chevron: ▸ rotates 90° when open (transition stills under prefers-reduced-motion — see the reduced-motion block below). */ details.thinking summary::before { content: "▸"; display: inline-block; transition: transform 0.15s ease; } details.thinking[open] summary::before { transform: rotate(90deg); } details.thinking summary:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; } /* The scratchpad is a live-tail, compact area (max-height keeps long reasoning from pushing the answer off-screen while open). */ details.thinking .thinking-text { padding: 0 0.75rem 0.75rem; color: var(--ink-soft); /* 6.9:1 on --surface */ font-size: 0.875rem; line-height: 1.55; max-height: 320px; overflow-y: auto; /* user-scrollable window (owner direction 2026-08-27, TODO.md L7): autoscroll follows the live tail only while the user is pinned near the window's bottom — the phase-17 pin, gated in app.js (task 02: THINKING_NEAR_BOTTOM_PX); scrolling up pauses the follow, returning to the bottom resumes it. */ } /* The scratchpad is compact: tighten the renderer's paragraph/list margins. */ details.thinking .thinking-text p, details.thinking .thinking-text ul { margin: 0 0 0.5rem; } /* Phase 117: the tool-call record folds into a single disclosure line, reusing the Thinking block's native
convention. Open while the turn is live (phase 87's elapsed suffix stays visible), folded at rest / on restore. The summary is a native focusable toggle — it inherits the house 3px :focus-visible ring (see details.thinking summary). Accent rides the TEXT (var(--accent-ink), ≈10.4:1 on the surface) — text + color, never color alone (B5). No new literal. The chevron matches details.thinking summary::before so the two disclosures read as one family — the count text is the accessible label either way. */ .tool-calls-disclosure { margin: 0.25rem 0; } .tool-calls-summary { cursor: pointer; color: var(--accent-ink); font-size: 0.8rem; line-height: 1.4; padding: 0.15rem 0; list-style: none; /* the native marker is redundant with the count text */ } .tool-calls-summary::-webkit-details-marker { display: none; } /* CSS chevron: ▸ rotates 90° when open — the same family treatment as details.thinking summary::before (the transition stills under prefers-reduced-motion — see the reduced-motion block below). */ .tool-calls-summary::before { content: "▸"; display: inline-block; margin-right: 0.5rem; transition: transform 0.15s ease; } .tool-calls-disclosure[open] .tool-calls-summary::before { transform: rotate(90deg); } /* Agent tool-call lines (phase 37, deboxed in phase 117): one compact inline-flow row per `tool` SSE frame — in the same wrap as the Thinking block, above the answer, below the Thinking summary, inside the phase-117 disclosure. Deliberately distinct from the scratchpad: accent text (--accent-ink) vs the brand-ink summary. Contrast: --accent-ink on the surface ≈10.4:1 (11.6:1 on the page bg), and --ink on the surface 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 (72rem — the .container width at every viewport; no new container), and the rows are not interactive — no focus targets (the fold is the disclosure's native summary). */ .tool-calls { display: flex; flex-direction: column; gap: 0.25rem; } /* Phase 117 deboxed the line (owner visual-glitch report 2026-09-15): no card, no flex — the label + the inline `code` flow as ONE continuous run, so a long path wraps to the left edge like a normal sentence and the label no longer breaks mid-word ("Rea/ding" is gone). The accent now rides the TEXT color (var(--accent-ink)) instead of a border. */ .tool-call { color: var(--accent-ink); font-size: 0.8rem; line-height: 1.4; overflow-wrap: anywhere; } /* Phase 117 dechipped the path: it keeps the mono + ink treatment (var(--ink) on the surface ≈11.5:1 — AA) but loses the chip background — it is inline text in the line's run now. */ .tool-call code { font-family: var(--mono); font-size: 0.95em; color: var(--ink); overflow-wrap: anywhere; } /* Phase 87 (TODO.md L5): the latest tool line's visible "processing" suffix — ticking "(Ns)" while ≥5s of the turn's stream stay silent after the call; removed on the next frame (settle). ink-soft on the bubble surface (the AA pairing), small mono like every status line. */ .tool-elapsed { font-family: var(--mono); font-size: 0.75rem; color: var(--ink-soft); margin-left: 0.5rem; white-space: nowrap; } /* Phase 95 (task 02): the truncation marker on a Reading line — the " (truncated — showing N of M chars)" span the `tool_result` frame appends (live) or the restore/shared paths re-render from the stored record. Theme-neutral, NO new hue (the phase-92 zero-literal invariant): it borrows --ink-soft — the same AA-safe soft-ink the status suffixes use — so under phase 93's monochrome theme it grays automatically, and the marker stays TEXT, never color alone (B5). */ .tool-call .truncated-note { color: var(--ink-soft); } .msg-meta { font-size: 0.75rem; color: var(--ink-soft); padding-inline: 0.25rem; display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; } .source-chip { display: inline-flex; align-items: center; gap: 0.3rem; font-family: var(--mono); font-size: 0.72rem; background: var(--brand-soft); color: var(--brand-ink); border: 1px solid var(--line); border-radius: 999px; padding: 0.15rem 0.6rem; text-decoration: none; max-width: 100%; /* min-width: 0 so the nowrap pill ellipsizes instead of widening its row */ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .source-chip:hover { background: var(--brand-soft); text-decoration: underline; } /* Phase 113 (task 02): the related-docs row — the SECONDARY tier of scored docs (phase 113 task 01's usefulness bar demotes the sub-floor hits out of the citation surface; on a deflected turn the weak hits live HERE and the chip row is absent). It must never read as a citation: smaller mono links, DASHED border (the solid brand pill is the citation look), transparent fill, muted --ink-soft text — 8.6:1 on --bg (verified ≥4.5:1, WCAG 2.1 AA), palette variables only (phase-92 zero-literal: no new hue, the monochrome theme grays it automatically). The row joins .msg-meta (the .msg-body flex gap stacks it below the citation row — no extra margin) but the meta ACTIONS never do: app.js appends it last, after the first-row claimers. :focus-visible via the global 3px outline rule. The hover is deliberately flat — NO background swap (the chip's brand-soft hover is the citation affordance): only the ink-soft → ink step-up plus the underline. */ .related-docs-label { font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-soft); } .related-doc { display: inline-flex; align-items: center; font-family: var(--mono); font-size: 0.7rem; color: var(--ink-soft); background: transparent; border: 1px dashed var(--line); border-radius: 999px; padding: 0.12rem 0.55rem; text-decoration: none; max-width: 100%; /* min-width: 0 so the nowrap pill ellipsizes (same as the chips) */ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .related-doc:hover { color: var(--ink); /* ink on --bg 16.7:1 — the flat secondary hover */ text-decoration: underline; } /* "Maybe try" chips under a deflected bubble (phase 04). Unlike the onboarding row (which scrolls horizontally on mobile), this group wraps at every width: the chips are the actionable follow-up, not decoration. The pills themselves reuse .suggestion-chip (>=44px, brand-soft/ink). */ .maybe-try { display: flex; align-items: center; flex-wrap: wrap; gap: 0.45rem; padding-inline: 0.25rem; } /* ---------- Steering notes (phase 15) ---------- */ /* "Tune" button in the meta row of every completed brain bubble: ghost pill, ≥44px, right-aligned after the source chips. ink-soft on surface ≈6.9:1; hover pair brand-ink/brand-soft ≈6.9:1. */ .tune-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; min-height: 44px; margin-left: auto; padding: 0.35rem 0.8rem; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .tune-btn svg { width: 14px; height: 14px; display: block; } .tune-btn:hover { background: var(--brand-soft); color: var(--brand-ink); } /* Phase 49 (owner-locked 2026-08-29, TODO.md L4): the "Retry" button — the redo-in-place action in the meta row of the LAST brain bubble. The exact visual family of .tune-btn (same pill size/spacing, the global :focus-visible ring, >=44px via min-height) so the two meta actions read as a pair — but neutral: ink-soft -> ink on hover (Tune keeps the brand pair). The redo glyph rides currentColor. Contrast: ink-soft on --bg ~8.6:1, ink ~15:1, hover ink on --brand-soft ~15.7:1 — all AA. */ .retry-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; min-height: 44px; margin-left: auto; padding: 0.35rem 0.8rem; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .retry-btn svg { width: 14px; height: 14px; display: block; } .retry-btn:hover { background: var(--brand-soft); color: var(--ink); } /* Phase 59 (owner-locked 2026-08-31, TODO.md L3): the "Save as doc" button — the bottom-right action of every completed brain bubble's meta row (the JS gates: admin + a configured docs repo; this rule only styles). The exact visual family of .tune-btn / .retry-btn (same pill size/spacing, the global :focus-visible ring, >=44px via min-height) so the meta actions read as one set — the brand hover pair like Tune (a docs action), the file glyph rides currentColor. margin-inline-start: auto pushes it to the row's RIGHT edge (the TODO's "bottom right"; markLastRetryable keeps it rightmost when the last bubble also carries the Retry button). Contrast: ink-soft on --bg ~8.6:1, hover brand-ink on --brand-soft — AA, same as the family. */ .save-as-doc-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; min-height: 44px; margin-inline-start: auto; padding: 0.35rem 0.8rem; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .save-as-doc-btn svg { width: 14px; height: 14px; display: block; } .save-as-doc-btn:hover { background: var(--brand-soft); color: var(--brand-ink); } .save-as-doc-btn:disabled { opacity: 0.6; cursor: wait; } /* draft POST in flight */ /* Phase 48: the "Stopped" note in a stopped brain bubble's meta row: ink-soft on the surface bubble ≈6.9:1, the 10px filled-square glyph centered with the row (the Tune button shares the row), and non-interactive — no hover, no focus (pointer-events: none). The text carries the accessible meaning; the glyph is aria-hidden in the JS. */ .stopped-note { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--ink-soft); font-size: 0.75rem; font-weight: 600; white-space: nowrap; pointer-events: none; } .stopped-note svg { width: 10px; height: 10px; display: block; fill: currentColor; } /* Phase 120 (TODO.md L3–4): the "Failed" note in a failed brain bubble's meta row — the .stopped-note family in the theme's error color (text + color, never color alone — B5: the "Failed" label carries the meaning, the triangle glyph is aria-hidden decoration in the JS). --err-ink on the --surface bubble ≈9.0:1 (the same value as --brand-ink, computed 9.0:1 on --surface above) — well past 4.5:1 (WCAG 2.1 AA); the monochrome theme grays it automatically (token, not literal — phase-92 zero-literal). Non-interactive — no hover, no focus (pointer-events: none), the stopped note's way. The detail span WRAPS (the stopped note's nowrap fits a one-word label; a 500- char error detail must not blow out the 46rem chat column) — overflow-wrap: anywhere keeps long unbroken details inside the row. */ .failed-note { display: inline-flex; align-items: flex-start; gap: 0.3rem; max-width: 100%; color: var(--err-ink); font-size: 0.75rem; font-weight: 600; pointer-events: none; } .failed-note svg { width: 10px; height: 10px; margin-top: 0.15rem; display: block; flex: none; fill: none; stroke: currentColor; } .failed-detail { font-weight: 400; overflow-wrap: anywhere; } /* Inline tuning form under the bubble: labeled textarea + Save/Cancel (both ≥44px). Save = brand button (dark ink 5.2:1), Cancel = ghost. */ .tune-form { display: flex; flex-direction: column; gap: 0.5rem; background: var(--surface); border: 1px solid var(--brand-soft); border-radius: var(--radius-sm); padding: 0.75rem 0.85rem; } .tune-form label { font-size: 0.85rem; font-weight: 600; color: var(--ink-soft); } .tune-form textarea { font: inherit; font-size: 0.9rem; color: var(--ink); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.5rem 0.6rem; resize: vertical; min-height: 2.6rem; } .tune-form-actions { display: flex; gap: 0.5rem; } .tune-save { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0.4rem 1.1rem; border: 0; border-radius: var(--radius-sm); background: var(--brand); color: var(--bg); font: inherit; font-weight: 700; cursor: pointer; } .tune-save:hover:not(:disabled) { background: var(--brand-hover); } .tune-save:disabled { opacity: 0.6; cursor: wait; } .tune-cancel { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0.4rem 1rem; border-radius: var(--radius-sm); border: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; cursor: pointer; } .tune-cancel:hover { background: var(--brand-soft); color: var(--brand-ink); } /* Save confirmation (role=status): ok pair ≈10.6:1. */ .tune-saved { margin: 0.2rem 0 0 0.25rem; background: var(--ok-bg); color: var(--ok-ink); border: 1px solid color-mix(in srgb, var(--ok-ink) 35%, transparent); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-size: 0.85rem; font-weight: 600; } /* Inline save failure (role=alert): err pair ≈9.1:1 — form is kept. */ .tune-error { margin: 0 0 0 0.25rem; background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-size: 0.85rem; font-weight: 600; } /* Header panel above the messages: notes newest-first, per-note delete, designed empty state. */ .steering-panel { background: var(--surface); border: 1px solid var(--brand-soft); border-radius: var(--radius); box-shadow: var(--shadow); padding: 0.9rem 1.1rem 1rem; } .steering-panel-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.6rem; } .steering-panel-title { margin: 0; font-size: 1rem; font-weight: 700; color: var(--ink); } .steering-panel-sub { margin: 0; font-size: 0.82rem; color: var(--ink-soft); } .steering-list { list-style: none; margin: 0.65rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; } .steering-note { display: flex; align-items: stretch; gap: 0.6rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.35rem 0.4rem 0.35rem 0.8rem; } .steering-note-text { color: var(--ink); font-size: 0.9rem; line-height: 1.45; /* Notes may be multi-line instructions — keep line breaks as typed. */ white-space: pre-wrap; overflow-wrap: anywhere; flex: 1; min-width: 0; } .steering-delete { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; min-width: 44px; flex: 0 0 auto; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); cursor: pointer; } .steering-delete svg { width: 16px; height: 16px; display: block; } .steering-delete:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .steering-delete:disabled { opacity: 0.5; cursor: wait; } .steering-empty { margin: 0.65rem 0 0; color: var(--ink-soft); font-size: 0.88rem; } /* Phase 34 task 04 (visual pass): on the non-chat pages the panel is a direct child of
(not inside a .container) — pin it to the same 72rem content column as everything else so it never renders as a full-viewport band. The chat page's panel sits inside .chat-shell's container, so this selector only hits the direct-child placement. */ .app-main > .steering-panel { width: calc(100% - 2.5rem); max-width: 69.5rem; margin-inline: auto; } /* ---------- Global tuning page (phase 27) ---------- */ /* /tuning.html: create / edit / delete steering notes without a chat 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 { display: flex; flex-direction: column; gap: 1.25rem; flex: 1; } /* Create form — the composer's surface as a vertical card: labeled textarea (visually-hidden label; the placeholder carries the visible hint) + the brand "Add note" button (dark ink on brand: 5.2:1 — never white on brand, 3.7:1, fails). */ #tune-form { display: flex; flex-direction: column; align-items: flex-start; gap: 0.6rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 0.9rem 1rem 1rem; } #tune-form:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); } #tune-note { width: 100%; font: inherit; font-size: 0.95rem; color: var(--ink); background: transparent; border: 0; padding: 0.2rem 0.1rem; resize: vertical; min-height: 4.6rem; } #tune-note::placeholder { color: var(--ink-soft); } #tune-save { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0.4rem 1.2rem; border: 0; border-radius: var(--radius-sm); background: var(--brand); color: var(--bg); /* dark ink on brand: 5.2:1 */ font: inherit; font-weight: 700; cursor: pointer; } #tune-save:hover:not(:disabled) { background: var(--brand-hover); } #tune-save:disabled { opacity: 0.6; cursor: wait; } /* Notes list — the phase-15 steering panel's language at full column width: rows are flex (text flexes + ellipsizes, actions shrink-0) with a bottom divider (the last row keeps none). */ .tuning-panel { background: var(--surface); border: 1px solid var(--brand-soft); border-radius: var(--radius); box-shadow: var(--shadow); padding: 0.9rem 1.1rem 1rem; } .tuning-panel-title { margin: 0; font-size: 1rem; font-weight: 700; color: var(--ink); } .tuning-list { list-style: none; margin: 0.65rem 0 0; padding: 0; display: flex; flex-direction: column; } .tuning-note { display: flex; align-items: center; gap: 0.6rem; padding: 0.35rem 0; border-bottom: 1px solid var(--line); } .tuning-note:last-child { border-bottom: 0; } /* The note text is the row's flex citizen: it takes every spare pixel and ellipsizes (the full text is one Edit away — the inline form). */ .tuning-note-text { color: var(--ink); font-size: 0.9rem; line-height: 1.45; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Row actions: icon + label, >=44px, shrink-0. Edit = brand pair on hover (brand-ink/brand-soft 6.9:1); Delete = err pair on hover (9.1:1), consistent with .steering-delete. */ .tuning-edit, .tuning-delete { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; min-height: 44px; min-width: 44px; flex: 0 0 auto; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .tuning-edit svg, .tuning-delete svg { width: 14px; height: 14px; display: block; } .tuning-edit:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand-soft); } .tuning-delete:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .tuning-edit:disabled, .tuning-delete:disabled { opacity: 0.5; cursor: wait; } /* Inline edit: the row swaps to a vertical card — the text span is hidden and replaced by the .tuning-edit-form (textarea pre-filled + Save/Cancel, reusing the phase-15 .tune-save/.tune-cancel styles); the row-level Edit/Delete buttons step aside for the form's own. */ .tuning-note.is-editing { flex-direction: column; align-items: stretch; gap: 0.5rem; background: var(--surface); border: 1px solid var(--brand-soft); border-radius: var(--radius-sm); padding: 0.6rem 0.7rem; } .tuning-note.is-editing .tuning-note-text, .tuning-note.is-editing .tuning-edit, .tuning-note.is-editing .tuning-delete { display: none; } .tuning-edit-form { display: flex; flex-direction: column; gap: 0.5rem; width: 100%; } .tuning-edit-input { font: inherit; font-size: 0.9rem; color: var(--ink); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.5rem 0.6rem; resize: vertical; min-height: 2.6rem; } .tuning-edit-input::placeholder { color: var(--ink-soft); } .tuning-edit-form-actions { display: flex; gap: 0.5rem; } /* Per-action status pills (role=status / role=alert), the phase-15 pairs: ok 10.6:1, err 9.1:1. */ .tuning-saved { background: var(--ok-bg); color: var(--ok-ink); border: 1px solid color-mix(in srgb, var(--ok-ink) 35%, transparent); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-size: 0.85rem; font-weight: 600; } .tuning-error { background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-size: 0.85rem; font-weight: 600; } #tune-empty { margin: 0.65rem 0 0; color: var(--ink-soft); font-size: 0.88rem; font-style: italic; text-align: center; } /* typing indicator */ /* align-items: center — the dots stay vertically centered when the phase-87 "Ns" hint (a full text line) makes the row taller; with only the three 8px dots the centering is a visual no-op. */ .typing { display: inline-flex; align-items: center; gap: 5px; padding: 0.9rem 1rem; } .typing span { width: 8px; height: 8px; border-radius: 50%; background: var(--ink-soft); opacity: 0.5; animation: typing 1.2s infinite ease-in-out; } .typing span:nth-child(2) { animation-delay: 0.15s; } .typing span:nth-child(3) { animation-delay: 0.3s; } @keyframes typing { 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } 30% { transform: translateY(-5px); opacity: 1; } } /* Phase 87 (TODO.md L5): the visible pre-token elapsed hint — the 10s thinking clock promoted from aria-only to sighted users. ink-soft on the bubble surface (the AA pairing), small mono like every status line. Plain text: no animation, no motion opt-out. The span is a sibling of the dots inside the same .typing bubble, so it resets the dot geometry (.typing span) it would otherwise inherit. The selector MUST stay ".typing span.typing-elapsed": a bare ".typing-elapsed" (0,1,0) LOSES the specificity war to the dot rule ".typing span" (0,1,1) — every reset below (width/height/border- radius/background/opacity/animation) would be overridden and the hint would render as an 8×8px bouncing dot with the "Ns" text wrapping one character per line (phase-87 regression, 2026-09). */ .typing span.typing-elapsed { width: auto; height: auto; border-radius: 0; background: none; opacity: 1; animation: none; font-family: var(--mono); font-size: 0.75rem; color: var(--ink-soft); margin-left: 0.5rem; } @media (prefers-reduced-motion: reduce) { .typing span { animation: none; opacity: 0.7; } /* Phase 87 (TODO.md L5): the elapsed hint is plain text, not motion — it keeps full opacity (the AA pairing) under reduced motion. */ .typing span.typing-elapsed { opacity: 1; } /* Phase 17 thinking block: the chevron stills (no rotation motion). */ details.thinking summary::before { transition: none; } /* Phase 117 tool-calls disclosure: the same chevron, the same still. */ .tool-calls-summary::before { transition: none; } } /* Phase 117 (owner request 2026-09-15, live-mockup-confirmed — the phase-109 D16 dot cue replaced by a BRAIN-WAVE; the owner picked the ECG over the first equalizer mock): a compact trace (P bump, QRS spike, T bump — 49px wide, 8px flat tails; the middle is never rescaled) in index.html as a static SVG. .bw-ghost is the always-visible dim trace (the shape reads even when the sweep is between peaks); .bw-pulse is the same path in brand with a 42/140 stroke-dash segment that TRAVELS the path — the classic heart-monitor draw (the bwdraw keyframe below). Still the phase-109 D16 contract: visible for the ENTIRE active turn (send → terminal frame), its hidden attribute owned solely by app.js's setUiState (shown iff inFlight — hidden by construction in every terminal state, PLAN §7.4). DECORATIVE (aria-hidden in index.html): #send-status carries the state's meaning, so contrast is N/A here (§7.2 "text + color, never color alone" — the state TEXT stays in #send-status). The container is 44px = .send-btn min-height (house cross-reference — keep in lockstep), the trace centered against the button. The NEW bwdraw family is deliberate: the phase-109 "reuse the typing animation" pin is superseded by the owner's redesign. The loader sits LEFT of the button in the markup (index.html) so its appearance/disappearance never shifts the button. */ .turn-loader { display: inline-flex; align-items: center; /* center the trace against the button */ width: auto; height: 44px; /* = .send-btn min-height (44px) — keep in lockstep */ background: none; border-radius: 0; opacity: 1; align-self: flex-end; /* the composer row is flex-end aligned */ animation: none; /* the sweep carries the motion, not the container */ } .turn-loader svg { width: 49px; height: 32px; overflow: visible; /* the QRS spike (y=3) may exceed the box top */ } .turn-loader .bw-ghost { fill: none; stroke: var(--ink-soft); stroke-opacity: 0.3; /* the dim always-visible trace */ stroke-width: 2; } .turn-loader .bw-pulse { fill: none; stroke: var(--brand); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; /* The sweep: a 42/140 bright segment on the pathLength-normalized path — the dash cycle (42 + 98) equals the normalized length, so the bwdraw loop is seamless (no visible restart point). */ stroke-dasharray: 42 98; animation: bwdraw 0.9s linear infinite; } @keyframes bwdraw { from { stroke-dashoffset: 140; } to { stroke-dashoffset: 0; } } /* Phase 117: reduced motion — the sweep is STILLED (the full trace shows static, no travel — §7.2 house law), never HIDDEN: visibility is owned by the JS hidden attribute in setUiState (D16), not by CSS. This override MUST sit AFTER the main rules above: equal specificity, and the later rule wins the cascade — a media block placed before the main rule would lose the cascade and the sweep would keep traveling under reduced motion (the phase-109 cascade pitfall, carried over). */ @media (prefers-reduced-motion: reduce) { .turn-loader .bw-pulse { animation: none; stroke-dasharray: none; } } /* ---------- Empty state & suggestions ---------- */ .empty-state { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 2.5rem 1.75rem; text-align: center; margin-top: 1rem; } .empty-state-glyph { color: var(--brand-ink); width: 44px; height: 44px; margin-inline: auto; } .empty-state-glyph svg { width: 44px; height: 44px; display: block; } .empty-state-title { margin: 0.8rem 0 0.4rem; font-size: 1.5rem; color: var(--ink); } .empty-state-sub { margin: 0 auto 1.25rem; max-width: 34rem; color: var(--ink-soft); } .empty-state-sub code { font-family: var(--mono); font-size: 0.85em; background: var(--brand-soft); padding: 0.1em 0.35em; border-radius: 5px; } .suggestions { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; list-style: none; margin: 0; padding: 0; } /* The shared chip pill — both chip rows (the onboarding row in the empty state and the "Maybe try" row under a deflected bubble) reuse it. Phase 104 (owner 2026-09-12, A1): a chip is ONE line at every viewport width — `white-space: nowrap` stops a 400-char question from wrapping the pill into a multi-line "chonk"; `overflow: hidden` zeroes the flex item's automatic minimum size (min-width:auto would pin the item to the full text width), so `max-width: 100%` binds and `text-overflow: ellipsis` clips the text at the row edge — in the desktop wrap row 100% is the chat column, in the ≤640px row (nowrap + overflow-x: auto) 100% is the VISIBLE width and the row scrolls. That clipping is the phase-07 overflow fix the old "Maybe try" chip override used to carry (min-width:0 + max-width: 100%) — now fully subsumed here and deleted, provenance folded into this comment so the history lives with the contract. The full text stays one hover away (the title tooltip, phase 104 task 02). */ .suggestion-chip { font: inherit; font-size: 0.92rem; font-weight: 600; color: var(--brand-ink); background: var(--brand-soft); border: 1px solid var(--line); border-radius: 999px; padding: 0.55rem 1rem; min-height: 44px; cursor: pointer; transition: background 0.15s ease, transform 0.05s ease; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; min-width: 0; } .suggestion-chip:hover { background: var(--brand-soft); } .suggestion-chip:active { transform: scale(0.98); } /* ---------- 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 — 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 reached. Before this, a long conversation put the composer below the fold while phase 42 forbids auto-scrolling a streaming reply — the Stop control could literally not be reached ("it runs away from the user as they try to click stop"). The pin is TWO rules working together — sticky alone is not enough (owner revision 2026-08-30: "the chat message-input textarea should be at the bottom of the screen"). `position: sticky` can only pull the box UP to the viewport's bottom edge while the document overflows; it never pushes it DOWN, so on an empty/short chat the box still sat mid-screen. `.messages { flex: 1 1 auto }` supplies the missing half: it puts the box's resting position at the bottom of the full-height column, and sticky takes over the moment the conversation overflows. The offset is the notch-aware safe-area inset (`env()` resolves to 0, or falls back to 0 where unsupported, so the box sits flush with the viewport bottom; ≤640px keeps its own `main { padding-bottom: env(safe-area-inset-bottom) }`). CSS-only by design: no DOM change, no JS, and NO new scroll call site (the phase-42 never-auto-scroll contract stays intact — `scrollReveal` is still the one page scroll in app.js). The solid `--surface` background, border, radius and shadow are kept so messages scrolling behind the pinned box never show through it, and no z-index is added: DOM order already paints the composer over `.messages`, it never overlaps the sticky header (z 20) and stays under the z-1000 document modal. Phase 65 (2026-09-01, `TODO.md` L3, owner-locked A1): the pin now belongs to the .chat-bottom wrapper (rule below) — the row + this form are ONE sticky unit; this composer's own sticky declaration is kept, redundant inside the wrapper but pinned as a computed style by tests/e2e/test_pinned_composer.py. */ /* 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 (A1): the pills are literally always at the bottom of the screen, even while the reader is scrolled up through a long conversation (the rejected in-flow alternative would scroll away). The sticky range is the .chat-shell containing block (the wrapper's parent) — the same range the composer used to have alone — and the .messages flex-grow still rests the WHOLE unit at the screen bottom on short chats (the wrapper is the last shell child, so the grow pushes it down). The shell's 1rem gap applies only ABOVE the unit, from .messages — the 0.5rem gap here is the row↔composer spacing INSIDE it. No z-index: the phase-12 sticky header (z 20) and the phase-46 dropdown always stay on top, and at the document bottom the unit settles back into flow above the footer (the composer's opaque surface keeps messages hidden where the box overlaps them). The composer's own sticky rule below stays — redundant here (its containing block is the wrapper, so it cannot shift within the unit), 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); } /* Phase 88: pre-boot, the composer cluster is NOT sticky. The sticky promotion (a compositor layer) is deferred out of the first layout commit — on real Android, the cluster's sticky layer born at first commit was eating the hamburger's click (owner bug report 2026-09-08; the client-side switch into chat — same layer, born on a settled page — always worked). app.js adds .chat-booted two frames after boot settles; no-JS keeps the cluster static, which in the only no-JS state (empty, 100dvh document) sits exactly where the pin would. */ #view-chat:not(.chat-booted) .chat-bottom, #view-chat:not(.chat-booted) .composer { position: static; } /* Phase 104 (owner 2026-09-12, A4): the question-length counter — right-aligned above the composer, a child of the .chat-bottom sticky unit (between the actions row and the form), hidden until 80% of the 4,000-char cap (app.js updateCharCount). It sits on the APP background behind the transparent .chat-bottom unit, so both pairings are verified against --bg: --ink-soft on --bg = 8.6:1 (≥4.5:1, WCAG AA) and the .is-max state's --err-ink on --bg = 10.4:1 (AA). The .is-max state ALSO changes the copy ("— character limit") — text + color, never color alone (B3). */ .char-count { margin: 0; text-align: right; font-size: 0.75rem; line-height: 1.2; color: var(--ink-soft); } .char-count.is-max { color: var(--err-ink); } .composer { display: flex; align-items: flex-end; gap: 0.6rem; position: sticky; bottom: env(safe-area-inset-bottom, 0); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 0.6rem; } .composer:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); } .composer textarea { flex: 1; font: inherit; color: var(--ink); border: 0; resize: none; max-height: 12rem; padding: 0.55rem 0.5rem; background: transparent; } .composer textarea::placeholder { color: var(--ink-soft); } .send-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; min-width: 84px; min-height: 44px; border: 0; border-radius: var(--radius-sm); background: var(--brand); /* Dark ink on brand (5.2:1) — never white on brand (3.7:1, fails). */ color: var(--bg); font: inherit; font-weight: 700; cursor: pointer; padding-inline: 1rem; } .send-btn:hover:not(:disabled) { background: var(--brand-hover); } .send-btn:disabled { background: var(--brand-busy); cursor: not-allowed; } /* Phase 48 (2026-08-29, TODO.md L3): the in-flight Stop treatment — one button, two roles. --brand-stop (the brand darkened 75% toward --bg; the built-in default mixes to ≈ (187, 50, 73)) with a white label = 5.8:1 (WCAG AA); the hover step (--brand at 55% toward --bg) holds 8.5:1. white is the ONE literal allowed outside :root — the dark bg ink on the stop fill is only ≈3.4:1 (fails AA). Same radius/height/ hit target as the Send state and the shared :focus-visible ring — the .is-stop class rides the same .send-btn element, and the later rules win the hover specificity tie. */ .send-btn.is-stop { background: var(--brand-stop); color: #fff; } .send-btn.is-stop:hover { background: color-mix(in srgb, var(--brand) 55%, var(--bg)); } /* Busy spinner: dark arc (--bg) on the --brand-busy button ≈ 11.3:1 (well above the 9:1 floor the phase-48 note pinned for the arc). */ .spinner { width: 16px; height: 16px; border: 2.5px solid color-mix(in srgb, var(--bg) 30%, transparent); border-top-color: var(--bg); border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2s; } } /* ---------- Banners ---------- */ .kb-banner { display: flex; align-items: center; gap: 0.5rem; background: var(--accent-bg); color: var(--accent-ink); border: 1px solid var(--accent-line); border-radius: var(--radius-sm); padding: 0.6rem 0.9rem; font-size: 0.92rem; font-weight: 600; } .kb-banner.is-error { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .kb-banner svg { width: 18px; height: 18px; flex: 0 0 auto; display: block; } /* Phase 53 (task 05): the stale-saved-chat banner — the .kb-banner FAMILY (the section carries both classes: same flex row, accent tokens — accent-ink on accent-bg 9.5:1 — 18px glyph), but the leading mark is the REDO glyph, distinct from the warning triangle. It sits directly after #kb-banner in .chat-shell, so when both are visible the empty-KB banner keeps the top slot and the stale banner stacks directly below (the flex column's gap spaces them). The text takes the row; the Regenerate pill right-aligns (margin-left: auto) and the row wraps only when it must. The pill is the EXACT brand-pill family of Save/Share: solid --brand, --bg text (5.2:1, AA), borderless, 999px radius, ≥44px target, hover lightens the brand fill; the 16px redo glyph is the phase-49 Retry asset. The ≤640px block below makes the pill a full-width row. */ .stale-banner { flex-wrap: wrap; } .stale-banner > span { flex: 1 1 auto; } .stale-regenerate { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; min-height: 44px; margin-left: auto; padding: 0.5rem 0.9rem; border-radius: 999px; border: 0; background: var(--brand); color: var(--bg); font: inherit; font-weight: 700; font-size: 0.95rem; white-space: nowrap; cursor: pointer; } .stale-regenerate:hover { background: var(--brand-hover); color: var(--bg); } .stale-regenerate:disabled { opacity: 0.6; cursor: wait; } .stale-regenerate svg { width: 16px; height: 16px; display: block; } /* Phase 111 (task 01): the banner Retry button — the same pill treatment as .stale-regenerate (brand fill, ≥44px, 999px radius, redo glyph) so the banner's action reads as the same component family. The kb-banner flex row + gap already handles inline spacing after #kb-banner-text. */ .banner-retry { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; min-height: 44px; margin-left: auto; padding: 0.5rem 0.9rem; border-radius: 999px; border: 0; background: var(--brand); color: var(--bg); font: inherit; font-weight: 700; font-size: 0.95rem; white-space: nowrap; cursor: pointer; } .banner-retry:hover { background: var(--brand-hover); color: var(--bg); } .banner-retry:disabled { opacity: 0.6; cursor: wait; } .banner-retry svg { width: 16px; height: 16px; display: block; } /* Phase 55 (task 04): the share-success toast (TODO.md L5). Top-right, just under the sticky header (--header-h + a small offset — the variable already steps 64px → 58px at ≤640px), z-index 1000 (the modal overlay contract — above the header's 20 and the skip-link's 100). VISUAL ONLY (A4 owner-locked): the node ships aria-hidden from app.js — #send-status stays the a11y announcer (no double screen-reader read). Solid brand fill: --bg text on --brand = 5.2:1 (AA — the .new-chat-btn family); rounded, shadowed, one line of text that wraps under the small max-width. Hidden by default (opacity 0 + pointer-events: none — it never intercepts clicks when idle); .toast.is-visible (toggled by showToast) runs the entry: slide-down + fade (translateY(-8px) → 0, 200ms). */ .toast { position: fixed; top: calc(var(--header-h) + 0.75rem); right: 1rem; z-index: 1000; max-width: min(22rem, calc(100vw - 2rem)); padding: 0.6rem 1rem; border-radius: var(--radius-sm); background: var(--brand); color: var(--bg); font-size: 0.92rem; font-weight: 600; box-shadow: var(--shadow); opacity: 0; pointer-events: none; transform: translateY(-8px); transition: opacity 200ms ease, transform 200ms ease; } .toast.is-visible { opacity: 1; transform: translateY(0); } @media (prefers-reduced-motion: reduce) { /* No slide: BOTH states rest at transform: none (the .is-visible rule would otherwise out-specify the bare .toast); the opacity fade remains (120ms). */ .toast, .toast.is-visible { transform: none; } .toast { transition: opacity 120ms ease; } } /* ---------- Login page (phase 16) ---------- */ /* Centered card in the standard frame: one admin, one password. */ .login-shell { display: flex; justify-content: center; flex: 1; } .login-card { width: 100%; max-width: 26rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 2rem 2rem 2.25rem; } .login-card h1 { margin: 0 0 0.4rem; font-size: 1.6rem; } .login-sub { margin: 0 0 1.5rem; color: var(--ink-soft); } #login-form { display: flex; flex-direction: column; gap: 0.75rem; } #login-password { font: inherit; font-size: 1rem; color: var(--ink); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.55rem 0.75rem; min-height: 44px; } #login-password:focus-visible { border-color: var(--brand); } /* Brand button: dark ink on brand 5.2:1 (never white on brand). */ .login-submit { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; border: 0; border-radius: var(--radius-sm); background: var(--brand); color: var(--bg); font: inherit; font-weight: 700; cursor: pointer; padding-inline: 1rem; } .login-submit:hover:not(:disabled) { background: var(--brand-hover); } .login-submit:disabled { opacity: 0.6; cursor: wait; } /* Login failure (role=alert): err pair ≈9.1:1. */ .login-error { margin: 0.9rem 0 0; background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.5rem 0.8rem; font-size: 0.9rem; font-weight: 600; } /* ---------- Sources page ---------- */ .sources-shell { display: flex; flex-direction: column; gap: 1.25rem; flex: 1; } /* Phase 93 task 03 (TODO.md L3): "the header and description of each page needs a background - the grid makes it hard to read." Every page's h1 + description sits on a solid surface panel — the house card language (var(--surface) fill, itself tab-controlled, so a monochrome theme grays it automatically; the 1px --line border; the house radius). No new color literal, no blur (the phase-08 perf anchor). One rule covers the six shell views' .page-head (Global Tuning, Knowledge base, Git sources, Saved chats, Access tokens, Theme) + doc-edit.html, and the shared page's head (its h1 + lede are wrapped in the same class in shared.html). #view-chat carries no .page-head — its head is the navbar (audited, unchanged). The History page-head keeps its flex row (scoped below — the padding lives on the flex container, not the children). */ .page-head { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.25rem; } .page-head h1 { margin: 0 0 0.25rem; font-size: 1.7rem; } .page-head-row { display: flex; align-items: center; gap: 1rem; } .page-sub { margin: 0; color: var(--ink-soft); } .page-sub code { font-family: var(--mono); font-size: 0.85em; background: var(--brand-soft); padding: 0.1em 0.35em; border-radius: 5px; } /* ---------- Sync button (Sources page) ---------- */ .sync-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; min-height: 44px; padding: 0.5rem 0.9rem; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.95rem; white-space: nowrap; cursor: pointer; flex-shrink: 0; } .sync-btn:hover { background: var(--brand-soft); color: var(--brand-ink); } .sync-btn:disabled { opacity: 0.6; cursor: wait; } .sync-btn.is-error { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .sync-btn.is-error:hover { background: var(--err-bg); color: var(--err-ink); } .sync-icon { width: 16px; height: 16px; display: block; flex: 0 0 auto; } .sync-btn .sync-icon.is-spinning { animation: spin 1s linear infinite; } @media (prefers-reduced-motion: reduce) { .sync-btn .sync-icon.is-spinning { animation: none; } } /* Phase 64: the live-file label ("Syncing… (n/m)" / "Importing (n/m)") — a long source/relative/path ellipsizes inside the pill; the full path lives in the button title + #sync-result (the aria-live announcer). */ .sync-label { display: inline-block; max-width: min(16rem, 40vw); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; } /* Last-result announcer — soft ink, small mono */ .sync-result { display: block; color: var(--ink-soft); font-family: var(--mono); font-size: 0.8rem; white-space: nowrap; padding-block: 0.5rem; } /* Sync failure modal — lazily created by sources.js, appended to . Error surface: --err-bg with --err-line border, title in --ink, error text in --err-ink (9.1:1 on --err-bg). Backdrop dims with --bg at 82%. z-index 1000 (same overlay contract as doc-modal). */ .sync-modal-backdrop { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 1rem; background: color-mix(in srgb, var(--bg) 82%, transparent); visibility: hidden; opacity: 0; transition: opacity 120ms ease; } .sync-modal-backdrop.is-open { visibility: visible; opacity: 1; } .sync-modal { position: relative; width: 100%; max-width: 28rem; background: var(--err-bg); border: 1px solid var(--err-line); border-radius: var(--radius); box-shadow: var(--shadow-lg); padding: 1.1rem 3rem 1.25rem 1.25rem; } #sync-modal-title { margin: 0 0 0.6rem; font-size: 1.1rem; color: var(--ink); } #sync-modal-error { margin: 0 0 1rem; font-family: var(--mono); font-size: 0.9rem; color: var(--err-ink); overflow-wrap: anywhere; } .sync-modal-close { position: absolute; top: 0.3rem; right: 0.3rem; display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; padding: 0; border-radius: 999px; border: 1px solid var(--err-line); background: transparent; color: var(--err-ink); font-size: 1.2rem; line-height: 1; cursor: pointer; } .sync-modal-close:hover { background: color-mix(in srgb, var(--err-line) 15%, transparent); } @media (prefers-reduced-motion: reduce) { .sync-modal-backdrop { transition: none; } } .stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0.9rem; } .stat-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 1.1rem 1.25rem; display: flex; flex-direction: column; gap: 0.15rem; } /* Mono stat values — technical readout (phase 08). */ .stat-value { font-family: var(--mono); font-size: 2rem; font-weight: 800; color: var(--brand-ink); line-height: 1.1; } .stat-value-sm { font-size: 1.15rem; font-weight: 700; } .stat-label { color: var(--ink-soft); font-size: 0.88rem; font-weight: 600; } /* Phase 16: anonymous sign-in gate — the designed replacement for the catalog (stat cards + table) until the admin signs in. */ .sources-gate { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.4rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 2.5rem 1.75rem; } .sources-gate-glyph { color: var(--brand-ink); width: 44px; height: 44px; } .sources-gate-glyph svg { width: 44px; height: 44px; display: block; } .sources-gate h2 { margin: 0.6rem 0 0.3rem; font-size: 1.4rem; } .sources-gate-sub { margin: 0 auto; max-width: 30rem; color: var(--ink-soft); } .sources-gate-link { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; margin-top: 0.75rem; padding: 0.5rem 1.4rem; border-radius: 999px; background: var(--brand); color: var(--bg); /* dark ink on brand: 5.2:1 */ font-weight: 700; text-decoration: none; } .sources-gate-link:hover { background: var(--brand-hover); } /* Phase 79 (task 05): the in-app token gate — the gate surface of the two token-only pages (the shell + the document viewer, one shared .auth-gate markup each). A body-level FIXED overlay (the body-level doc-modal precedent): while visible it is the only CONTENT-level interactive surface — assets/token-gate.js locks the root (#main, inert) so focus and keyboard traversal stay inside the gate (WCAG, the inert-pair contract). Stacking (phase 85, TODO.md L3): z-index 15 — ABOVE all app content (static) but BELOW the sticky header (20) and its mobile dropdown (21, inside the header's stacking context): the unauthenticated visitor keeps a reachable hamburger + menu while the gate still covers and locks the app content (#main stays inert — the lock is JS, not z-order — phase 79). The skip-link (100) and the 1000-family overlays (doc-modal, sync backdrop) are unaffected — all above 15; the doc-modal can only be opened from the unlocked app anyway. Solid --bg: the canvas + grid texture live on (the body stays transparent), so the overlay reads as the app's own surface — no blur (the phase-08 perf anchor). The centered card reuses the #sources-gate visual language (surface card + hairline + glyph + heading + sub + action) in a tighter column. */ .auth-gate { position: fixed; inset: 0; z-index: 15; display: flex; /* the card is the only in-flow child — margin: auto centers it */ overflow-y: auto; /* short viewports: the card scrolls fully into view */ background: var(--bg); } /* Explicit (the global [hidden] rule already wins — this is the documented, testable contract for the skeleton). */ .auth-gate[hidden] { display: none; } .auth-gate-card { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.4rem; /* margin:auto centers AND stays scrollable on overflow (the flex centering pitfall: align/justify center would clip the top of a taller-than-viewport card). */ margin: auto; width: min(28rem, calc(100vw - 2rem)); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 2.5rem 1.75rem; } .auth-gate-glyph { color: var(--brand-ink); width: 44px; height: 44px; } .auth-gate-glyph svg { width: 44px; height: 44px; display: block; } .auth-gate h2 { margin: 0.6rem 0 0.3rem; font-size: 1.4rem; } .auth-gate-sub { margin: 0; max-width: 24rem; color: var(--ink-soft); } /* The token form: mono input (the token is code — the house mono language), a visible focus ring (WCAG — the 3px :focus-visible outline at offset 0 + the brand border), and the house submit button (the .tune-save language: dark ink on brand, 5.2:1). Both targets are ≥44px (the touch-target contract). */ .auth-gate form { display: flex; flex-direction: column; gap: 0.6rem; width: 100%; margin-top: 0.9rem; } .auth-gate input { font-family: var(--mono); font-size: 0.95rem; color: var(--ink); /* 16.7:1 on --bg */ background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.65rem 0.8rem; min-height: 44px; width: 100%; } .auth-gate input::placeholder { color: var(--ink-soft); } .auth-gate input:focus-visible { outline-offset: 0; border-color: var(--brand); } .auth-gate-submit { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0.4rem 1.1rem; border: 0; border-radius: var(--radius-sm); background: var(--brand); color: var(--bg); /* dark ink on brand: 5.2:1 */ font: inherit; font-weight: 700; cursor: pointer; } .auth-gate-submit:hover:not(:disabled) { background: var(--brand-hover); } .auth-gate-submit:disabled { opacity: 0.6; cursor: wait; } /* The one-line error (role=alert) — the rose/danger family the house alerts use (--err-ink on --err-bg, 9.3:1). */ .auth-gate-error { margin: 0.75rem 0 0; background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; } /* The secondary "Sign in as admin" link reuses the .sources-gate-link pill language (its own margin-top: 0.75rem applies) — the guest's other door: the admin password login (the static ?next=/ is the no-JS fallback). */ /* Phase 86: the card is the containing block for the position:absolute .visually-hidden elements it hosts (the table
+ the Actions header span in #view-history / #view-tokens). Without a positioned ancestor their 1px boxes lay out against the INITIAL containing block — at the 640px table's right edge — and leak into the DOCUMENT's scrollable overflow (the page appeared "the width of the table", TODO.md L4; the Tokens view carried the identical defect). Zero-offset positioning changes no layout: the spans stay clipped by their own clip: rect(0 0 0 0) + 1px box, and cards without such spans (the RAG .docs-table, the git-sources table) render unchanged. */ .table-wrap { position: relative; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow-x: auto; } .docs-table { width: 100%; border-collapse: collapse; min-width: 640px; font-size: 0.93rem; } .docs-table th, .docs-table td { text-align: left; padding: 0.7rem 1rem; border-bottom: 1px solid var(--line); white-space: nowrap; } .docs-table th { background: var(--brand-soft); color: var(--brand-ink); font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.04em; position: sticky; top: 0; } .docs-table td:nth-child(2) { font-family: var(--mono); font-size: 0.82rem; max-width: 30rem; overflow: hidden; text-overflow: ellipsis; } .docs-table tbody tr:hover { background: var(--bg); } .docs-table tbody tr:last-child td { border-bottom: 0; } /* ---------- KB drill-down tree (phase 97, task 04) ---------- The RAG view lists the catalog the way the agent's `ls` sees it (the phase-94 concept, one end to end): #kb-crumb (the location breadcrumb — hidden at the top level), #kb-level (the current directory's STORED description — the surface-panel language of the phase-93 page heads), and #folders-table (the ONE folders/sources table — the .docs-table language; .kb-folders-table only re-styles the 2nd/4th column cells — phase 106 (task 08, D8) moved Description from 3rd to 4th, the Updated column taking 3rd). Phase-08 tokens only — NO new hue (the phase-92 monochrome invariant): brand-ink on surface 9.0:1, ink 13.8:1, ink-soft 5.1:1 (every pair AA). :focus-visible via the global 3px outline rule; row targets keep the .docs-table cell padding (>= 44px effective — the phase-08 touch floor). No CDN, system fonts. */ .kb-crumb { display: flex; flex-wrap: wrap; align-items: center; gap: 0.1rem 0.3rem; margin: 0.9rem 0 0; font-size: 0.92rem; } .kb-crumb-link { color: var(--brand-ink); /* 10.4:1 on --bg, 9.5:1 on the --brand-soft hover */ text-decoration: none; padding: 0.25rem 0.35rem; border-radius: var(--radius-sm); } .kb-crumb-link:hover { background: var(--brand-soft); text-decoration: underline; } .kb-crumb-current { color: var(--ink); /* 16.7:1 on --bg */ font-weight: 600; padding: 0.25rem 0.35rem; } .kb-crumb-sep { color: var(--ink-soft); } .kb-level { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.25rem; margin: 0.9rem 0 0.75rem; } .kb-level h2 { margin: 0 0 0.35rem; font-family: var(--mono); font-size: 1rem; color: var(--brand-ink); /* 9.0:1 on --surface — the path, in the path voice */ word-break: break-all; } .kb-level p { margin: 0; color: var(--ink-soft); } /* 5.1:1 on --surface */ .kb-folders-table { min-width: 480px; } .kb-folders-table td:nth-child(2) { font-family: var(--font); font-size: inherit; max-width: none; } /* Phase 106 (task 08, D8): the Updated column sits at 3rd (BETWEEN Documents and Description) — it takes the .docs-table cell defaults (nowrap text, the table ink: var(--ink) on the wrap's --surface — 13.8:1, the pair recorded at :root). NO new CSS: the one-line clamp below follows the Description cell, which moved to 4th. */ .kb-folders-table td:nth-child(4) { white-space: nowrap; /* phase 99 (D1): one line — the clamp's budget is the column width */ min-width: 18rem; max-width: 44rem; font-size: 0.88rem; } /* Phase 99 (task 01, D1): the Description cell is ONE flex row — the text span flexes + ellipsizes (the row height is one line, independent of the description's length), the Edit button stays fixed (flex: 0 0 auto — it never wraps to a second line). The gap replaces the button's old margin-left (the context-spacing rule below keeps only the level block's margin-top). The clamp is visual only: the full text stays in the DOM + on the span's title (hover). */ .kb-desc-cell { display: flex; align-items: center; gap: 0.4rem; min-width: 0; } .kb-desc-text { flex: 1 1 auto; min-width: 0; /* lets the flex item shrink — what engages the ellipsis (the .sync-label pattern) */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .folder-link { color: var(--brand-ink); /* 9.0:1 on --surface (the .doc-link pair, row variant) */ text-decoration: none; border-radius: var(--radius-sm); } .folder-link:hover, .folder-link:focus-visible { background: var(--brand-soft); text-decoration: underline; } /* ---------- KB folder-description editor (phase 97, task 05) ---------- The phase-57 edit affordance on the RAG view's folder descriptions (the .kb-summary-* family, mirroring the viewer's .doc-summary-*): the Edit button (a 24px+ target; the [hidden] override beats its display rule while the editor is open), the inline editor (the 8rem-min textarea + Save/Cancel + the role=status live region). House dark-tech palette (phase-08 tokens), system fonts, no CDN; :focus-visible via the global 3px outline rule. No new hue (the phase-92 monochrome invariant) — every color is a var(). */ .kb-summary-edit { flex: 0 0 auto; display: inline-flex; align-items: center; min-height: 24px; padding: 0.15rem 0.7rem; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ font: inherit; font-weight: 600; font-size: 0.78rem; letter-spacing: 0.02em; cursor: pointer; } .kb-summary-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); } .kb-summary-edit[hidden] { display: none; } /* the hidden attr must beat the display above */ /* Context spacing: the row cell's button spacing comes from the .kb-desc-cell gap (phase 99, task 01 — the margin-left is gone); the level block's sits below the

. */ .kb-level-body .kb-summary-edit { margin-top: 0.5rem; } .kb-summary-editor { display: block; width: 100%; min-height: 8rem; /* the phase-57 spec */ padding: 0.6rem 0.8rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--bg); /* inset against the --surface panel / row */ color: var(--ink); /* 16.7:1 on --bg (AA) */ font: inherit; line-height: 1.5; resize: vertical; } /* Phase 99 (task 01): the open editor fills the .kb-desc-cell flex wrapper — as a flex item it needs the explicit grow + min-width: 0 (the width: 100% base above still serves the level block's non-flex .kb-level-body). */ .kb-desc-cell .kb-summary-editor { flex: 1 1 auto; min-width: 0; } .kb-summary-actions { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.75rem; } .kb-summary-save { display: inline-flex; align-items: center; min-height: 32px; padding: 0.35rem 0.95rem; border: 0; border-radius: 999px; background: var(--brand); color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */ font: inherit; font-weight: 600; font-size: 0.85rem; cursor: pointer; } .kb-summary-save:hover { background: var(--brand-hover); } /* the house hover lightening */ .kb-summary-save:disabled { opacity: 0.6; cursor: default; } /* one PATCH at a time */ .kb-summary-cancel { display: inline-flex; align-items: center; min-height: 32px; padding: 0.35rem 0.95rem; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ font: inherit; font-weight: 600; font-size: 0.85rem; cursor: pointer; } .kb-summary-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .kb-summary-status { margin: 0.6rem 0 0; font-size: 0.85rem; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ } .kb-summary-status:empty { margin-top: 0; } /* Phase 98 (task 04, D4): the row-cell "Summary pending" marker — the muted ink-soft pair (5.1:1 on --surface, AA); text + color, never color alone. The class sits on the EXISTING description text span, so the row cell's font-size/line-height apply — the marker must not change row height (no font/white-space overrides). No italic, no new hue (the phase-92 monochrome invariant). */ .kb-summary-pending { color: var(--ink-soft); } /* ---------- Git sources page (phase 35) ---------- /git-sources.html: the admin-only manager for the stored git source list (add / remove, git-sources table). Same full-width table language as the Sources page (no skinny single-column list), the phase-16 gate reused verbatim (.sources-gate classes), and the phase-27 form-card language for the add form. Every pair reuses the Phase-08 AA palette: dark ink on brand 5.2:1 (never white on brand, 3.7:1, fails), brand-ink/brand-soft 6.9:1, err 9.1:1, ink-soft >=6.9:1. Touch targets >=44px; :focus-visible via the global 3px outline rule. No filter: blur, no CDN, system fonts. */ .git-sources-shell { display: flex; flex-direction: column; gap: 1.25rem; flex: 1; } /* Add forms — the tuning form's surface as a single row: visible label + mono location input (git URLs may embed credentials, so the input is mono) + the brand button; wraps to a column at narrow widths (the <=640px block below). Phase 49: the archive upload form (#archive-upload-form, replacing the phase-38 local-directory form) reuses the git form's card + button rules VERBATIM — one form language for both; its file input carries its own rules below. */ #git-source-form, #archive-upload-form { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 0.9rem 1rem 1rem; } #git-source-form:focus-within, #archive-upload-form:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); } #git-source-form > label, #archive-upload-form > label { color: var(--ink); font-weight: 600; white-space: nowrap; } /* Phase 121 (task 03): the form-label "optional" hint — the muted ink-soft pair (5.1:1 on the label's surface, AA) at the label's 600 weight relaxed to 400 so the hint reads as secondary (it qualifies the name, it is not the name); small, inline. */ .field-hint { color: var(--ink-soft); font-weight: 400; font-size: 0.8rem; } /* #git-source-token (phase 121): the optional masked private-repo credential — the URL input's treatment VERBATIM (mono, >=44px target, brand focus); type=password masks the value in display. */ #git-source-url, #git-source-token { flex: 1; min-width: 14rem; min-height: 44px; font-family: var(--mono); font-size: 0.88rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.45rem 0.7rem; } #git-source-url::placeholder, #git-source-token::placeholder { color: var(--ink-soft); } #git-source-url:focus-visible, #git-source-token:focus-visible { outline-offset: 0; border-color: var(--brand); } #git-source-add, #archive-upload-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0.4rem 1.2rem; border: 0; border-radius: var(--radius-sm); background: var(--brand); color: var(--bg); /* dark ink on brand: 5.2:1 */ font: inherit; font-weight: 700; cursor: pointer; } #git-source-add:hover:not(:disabled), #archive-upload-btn:hover:not(:disabled) { background: var(--brand-hover); } #git-source-add:disabled, #archive-upload-btn:disabled { opacity: 0.6; cursor: wait; } /* Phase 49: the upload form's file control — mono-ish, on-surface, a >=44px touch target, :focus-visible via the global 3px outline rule (offset zeroed + brand border, exactly like the git URL input). The chosen filename renders in mono; the selector button keeps a plain surface chip. */ #archive-upload-file { flex: 1; min-width: 14rem; min-height: 44px; font-family: var(--mono); font-size: 0.88rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.3rem 0.5rem; } #archive-upload-file:focus-visible { outline-offset: 0; border-color: var(--brand); } #archive-upload-file::file-selector-button { min-height: 34px; margin-right: 0.6rem; padding: 0.3rem 0.9rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); /* 13.8:1 on surface */ font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer; } #archive-upload-file::file-selector-button:hover { border-color: var(--brand); color: var(--brand-ink); } /* The upload's success line (role=status): the sync-result shape ("2 added · 1 pruned") — ink-soft on surface (>=4.5:1), the dashed hint-box border marks it as a result, not an error; it drops onto its own row under the input like the error line. */ #archive-upload-result { flex-basis: 100%; margin: 0; color: var(--ink-soft); background: var(--surface); border: 1px dashed var(--line); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-size: 0.85rem; font-weight: 600; } /* The add form's inline error (role=alert): the err pair (9.1:1); flex-basis 100% drops it onto its own row under the input. */ .git-source-error { flex-basis: 100%; margin: 0; background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-size: 0.85rem; font-weight: 600; } /* Load failure (role=alert) + the retry action: a failed GET /api/git-sources must never leave a stuck page. The retry button keeps the err pair on the err surface (9.1:1). */ .git-source-load-error { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 0.7rem 0.9rem; font-size: 0.9rem; font-weight: 600; } .git-source-load-error > span { flex: 1; min-width: 12rem; } #git-sources-retry { min-height: 44px; padding: 0.4rem 1rem; border: 1px solid var(--err-line); border-radius: var(--radius-sm); background: transparent; color: var(--err-ink); font: inherit; font-weight: 700; cursor: pointer; } #git-sources-retry:hover { background: color-mix(in srgb, var(--err-line) 12%, transparent); } /* Env-fallback note (from_env: true — the table is empty and the list is BOR_GIT_SOURCES): the info chip in the theme palette — brand-soft surface, brand-ink text (6.9:1). */ .git-source-env-note { margin: 0; background: var(--brand-soft); color: var(--brand-ink); border: 1px solid var(--brand-soft); border-radius: var(--radius-sm); padding: 0.6rem 0.9rem; font-size: 0.88rem; } .git-source-env-note code { font-family: var(--mono); font-size: 0.85em; background: var(--surface); color: var(--brand-ink); /* 8.7:1 on surface */ padding: 0.1em 0.35em; border-radius: 5px; } /* Hint box (role=note): the page-sub styling family — ink-soft on surface (6.9:1), a dashed border marks it as guidance, not state. It names the Sync button: the action that clones the listed repos and prunes the removed ones (the phase scope boundary). */ .git-source-hint { margin: 0; color: var(--ink-soft); background: var(--surface); border: 1px dashed var(--line); border-radius: var(--radius-sm); padding: 0.6rem 0.9rem; font-size: 0.88rem; } /* ---------- Remove confirmation modal (phase 69, task 02) ---------- /git-sources.html: the in-app confirmation that replaces the native confirm() — the total-removal warning (the row, the source's indexed documents, and — for git clones and uploaded archives — the files on the server's disk). The .doc-modal overlay contract: 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 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 (err-ink on err-bg 9.3:1, the err-line border); the destructive button rides the err token family — the .tuning-delete / .steering-delete hover convention used as the resting state (err-ink on err-bg 9.3:1; the hover inverts to dark --bg on --err-line, 5.2:1 — both AA); Cancel is the ghost ink-soft family (5.1:1 on --surface). :focus-visible via the global 3px outline rule (no local suppression — focus lands on Cancel at open and is visible); both buttons >=44px; no animation (reduced-motion safe by construction). */ .remove-confirm { position: fixed; inset: 0; z-index: 1000; display: flex; /* the panel is the only in-flow child — margin: auto centers it */ } /* Explicit (the global [hidden] rule already wins — the documented, testable contract for the skeleton). */ .remove-confirm[hidden] { display: none; } .remove-confirm-backdrop { position: fixed; inset: 0; /* --bg at 82% — the doc-modal dim, no backdrop-filter (no-blur). */ background: color-mix(in srgb, var(--bg) 82%, transparent); } .remove-confirm-panel { /* position:relative lifts the panel above the fixed backdrop (positioned elements paint over in-flow siblings otherwise). */ position: relative; z-index: 1; margin: auto; width: min(var(--chat-column), calc(100vw - 2rem)); padding: 1.5rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-lg); } .remove-confirm-title { margin: 0 0 0.75rem; font-size: 1.25rem; line-height: 1.3; color: var(--ink); } /* The source's value (git URL or local path) — mono, wrapped (a long URL must not overflow the panel), --ink on --bg (16.7:1). */ .remove-confirm-source { display: block; margin: 0 0 0.75rem; padding: 0.5rem 0.65rem; font-family: var(--mono); font-size: 0.85rem; line-height: 1.5; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); overflow-wrap: anywhere; } .remove-confirm-copy { margin: 0; color: var(--ink); /* 13.8:1 on --surface */ } /* The in-modal failure line (role=alert): the err pair (err-ink on err-bg 9.3:1, the err-line border) — the .git-source-error banner language, boxed. */ .remove-confirm-error { margin: 0.75rem 0 0; padding: 0.5rem 0.65rem; color: var(--err-ink); background: var(--err-bg); border: 1px solid var(--err-line); border-radius: var(--radius-sm); } .remove-confirm-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1.1rem; } /* The two modal buttons: >=44px targets, the house 3px :focus-visible via the global outline rule (no local override). */ .remove-confirm-btn { min-height: 44px; min-width: 44px; padding: 0.55rem 1.1rem; border-radius: var(--radius-sm); font: inherit; font-weight: 600; font-size: 0.9rem; white-space: nowrap; cursor: pointer; } .remove-confirm-btn:disabled { opacity: 0.5; cursor: wait; } /* Cancel — the ghost ink-soft family (5.1:1 on --surface), like the row's Remove / .tuning-edit; the brand pair on hover (6.9:1). */ .remove-confirm-cancel { border: 1px solid var(--line); background: transparent; color: var(--ink-soft); } .remove-confirm-cancel:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); } /* "Remove source" — the destructive button on the err token family (the .tuning-delete / .steering-delete convention): err-ink on err-bg 9.3:1, the err-line border; the hover inverts to dark --bg on --err-line (5.2:1 — AA). */ .remove-confirm-remove { border: 1px solid var(--err-line); background: var(--err-bg); color: var(--err-ink); } .remove-confirm-remove:hover:not(:disabled) { background: var(--err-line); color: var(--bg); } /* ---------- Per-source ignore-paths editor (phase 89, task 05) ---------- The shell's Sources view: the page-local alertdialog that edits one source's ignore list (one path per line — the phase-89 A1 prefix rule, stated in plain words in the helper copy). The EXACT #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 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 the err pair (err-ink on err-bg 9.3:1, the err-line border); Cancel is the ghost ink-soft family (5.1:1 on --surface); Save is the solid brand family (--bg text on --brand 5.2:1 — the .new-chat-btn convention; the hover lightens the fill). Both buttons >=44px; :focus-visible via the global 3px outline rule (no local suppression — focus lands on Cancel at open and is visible); no animation (reduced-motion safe by construction). AA pairs: title / copy / label are --ink (or ink-soft) on --surface (13.8:1 / 5.1:1); the source value is --ink on --bg (16.7:1). Phase-08 tokens only; system fonts; no CDN. */ .ignore-editor { position: fixed; inset: 0; z-index: 1000; display: flex; /* the panel is the only in-flow child — margin: auto centers it */ } /* Explicit (the global [hidden] rule already wins — the documented, testable contract for the skeleton). */ .ignore-editor[hidden] { display: none; } .ignore-editor-backdrop { position: fixed; inset: 0; /* --bg at 82% — the doc-modal dim, no backdrop-filter (no-blur). */ background: color-mix(in srgb, var(--bg) 82%, transparent); } .ignore-editor-panel { /* position:relative lifts the panel above the fixed backdrop (positioned elements paint over in-flow siblings otherwise). */ position: relative; z-index: 1; margin: auto; width: min(var(--chat-column), calc(100vw - 2rem)); padding: 1.5rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-lg); } .ignore-editor-title { margin: 0 0 0.75rem; font-size: 1.25rem; line-height: 1.3; color: var(--ink); } /* The source's value (git URL or local path) — mono, wrapped (a long URL must not overflow the panel), --ink on --bg (16.7:1). */ .ignore-editor-source { display: block; margin: 0 0 0.75rem; padding: 0.5rem 0.65rem; font-family: var(--mono); font-size: 0.85rem; line-height: 1.5; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); overflow-wrap: anywhere; } .ignore-editor-copy { margin: 0 0 1rem; color: var(--ink); /* 13.8:1 on --surface */ } .ignore-editor-copy code { font-family: var(--mono); font-size: 0.85em; } /* Visible block label (WCAG — the textarea is NEVER labelled only via aria-label); ink-soft on --surface (5.1:1). */ .ignore-editor-label { display: block; margin-bottom: 0.35rem; color: var(--ink-soft); font-size: 0.88rem; font-weight: 600; } /* The box: full panel width, mono (the var(--mono) stack), theme- consistent border on --bg; the focus-visible ring is the global 3px outline (no local suppression); vertical resize for long lists (up to 200 entries, A4). */ .ignore-editor-textarea { display: block; width: 100%; min-height: 9rem; padding: 0.55rem 0.65rem; font-family: var(--mono); font-size: 0.85rem; line-height: 1.5; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); resize: vertical; } /* Phase 121 (task 03): the editor's masked token field — the textarea's box language (full panel width, --bg fill, the line border, mono) at the 44px touch floor; the label's top margin keeps the field pair off the textarea. type=password + autocomplete=off live in the markup (a PAT is not a site credential — no browser save offer); the focus ring is the global 3px outline rule. */ .ignore-editor-token-label { margin-top: 0.9rem; } .ignore-editor-token { display: block; width: 100%; min-height: 44px; padding: 0.55rem 0.65rem; font-family: var(--mono); font-size: 0.85rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); } /* The in-dialog failure line (role=alert): the err pair (err-ink on err-bg 9.3:1, the err-line border) — the .remove-confirm-error language. */ .ignore-editor-error { margin: 0.75rem 0 0; padding: 0.5rem 0.65rem; color: var(--err-ink); background: var(--err-bg); border: 1px solid var(--err-line); border-radius: var(--radius-sm); } .ignore-editor-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1.1rem; } /* The two dialog buttons: >=44px targets, the house 3px :focus-visible via the global outline rule (no local override). */ .ignore-editor-btn { min-height: 44px; min-width: 44px; padding: 0.55rem 1.1rem; border-radius: var(--radius-sm); font: inherit; font-weight: 600; font-size: 0.9rem; white-space: nowrap; cursor: pointer; } .ignore-editor-btn:disabled { opacity: 0.5; cursor: wait; } /* Cancel — the ghost ink-soft family (5.1:1 on --surface), like the remove dialog's Cancel; the brand pair on hover (12.4:1 on brand-soft). */ .ignore-editor-cancel { border: 1px solid var(--line); background: transparent; color: var(--ink-soft); } .ignore-editor-cancel:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); } /* "Save" — the solid brand family (the .new-chat-btn convention): --bg text on --brand (5.2:1 — AA); the hover lightens the brand fill (the .new-chat-btn hover pair). */ .ignore-editor-save { border: 0; background: var(--brand); color: var(--bg); } .ignore-editor-save:hover:not(:disabled) { background: var(--brand-hover); } /* The list: the Sources page's table pattern — full width in the 72rem frame, surface card, horizontally scrollable wrapper (the URL column never wraps or ellipsizes: long URLs, credentials included, scroll the wrapper instead of truncating). */ #git-sources-table-wrap { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow-x: auto; } .git-sources-table { width: 100%; border-collapse: collapse; min-width: 560px; font-size: 0.93rem; } .git-sources-table th, .git-sources-table td { text-align: left; padding: 0.7rem 1rem; border-bottom: 1px solid var(--line); white-space: nowrap; } .git-sources-table th { background: var(--brand-soft); color: var(--brand-ink); font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.04em; position: sticky; top: 0; } /* Location cell: mono at the Sources-table size; the is plain (no chip background — the cell IS the mono readout). Phase 38: leads with the kind badge (Git/Local) — the kinds are told apart by TEXT as much as color (WCAG 1.4.1), and both badge pairs are AA: brand-ink on brand-soft 6.9:1 (Git), ok-ink on ok-bg 10.6:1 (Local). */ .git-sources-table td.git-source-url-cell { font-family: var(--mono); font-size: 0.82rem; } .git-sources-table td.git-source-url-cell code { font-family: inherit; } .git-source-kind { display: inline-block; margin-right: 0.55rem; padding: 0.08rem 0.5rem; border-radius: 999px; font-family: var(--font); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; vertical-align: 0.06em; white-space: nowrap; } .git-source-kind.is-git { color: var(--brand-ink); background: var(--brand-soft); } .git-source-kind.is-local { color: var(--ok-ink); background: var(--ok-bg); } .git-sources-table tbody tr:hover { background: var(--bg); } .git-sources-table tbody tr:last-child td { border-bottom: 0; } /* Per-row Remove: the tuning row-action language (icon + label, >=44px) with the Delete hover pair (err 9.1:1). */ .git-source-remove { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; min-height: 44px; min-width: 44px; flex: 0 0 auto; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .git-source-remove svg { width: 14px; height: 14px; display: block; } .git-source-remove:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .git-source-remove:disabled { opacity: 0.5; cursor: wait; } /* Phase 89: the per-row "Ignore paths" trigger (makeRow — left of Remove): the .git-source-remove idiom (same size/spacing, >=44px target) in a NEUTRAL secondary fill — it must read distinct from the destructive Remove (text label included, never icon-only; the hover takes the brand-soft pair, 12.4:1 — Remove hovers to the err pair, 9.3:1). */ .git-source-ignore { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; min-width: 44px; flex: 0 0 auto; padding: 0.35rem 0.7rem; margin-right: 0.4rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .git-source-ignore:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); } /* Phase 89: the "N ignored" count tag — next to the location in the Source cell (TEXT + a distinct background, never color alone — WCAG 1.4.1): --ink on --bg (16.7:1) inside the --surface card; the line border keeps the chip legible when the row hover swaps the cell onto --bg too. */ .git-source-ignore-count { display: inline-block; margin-left: 0.55rem; padding: 0.08rem 0.5rem; border: 1px solid var(--line); border-radius: 999px; font-size: 0.72rem; font-weight: 600; color: var(--ink); background: var(--bg); white-space: nowrap; } /* Phase 105 (A5): the per-row "Hidden" checkbox — the actions cell, LEFT of the "Ignore paths" button (JS builds it, git-sources.js makeRow). A native labeled checkbox: the visible "Hidden" text + the checkbox's own aria-label (full source value). ~44px hit height matches the action buttons; :focus-visible via the GLOBAL rule (L146) — no per-control ring needed. */ .git-source-hidden { display: inline-flex; align-items: center; gap: 0.4rem; height: 44px; padding: 0 0.5rem; font-size: 0.8rem; color: var(--ink); cursor: pointer; user-select: none; white-space: nowrap; } /* --brand checkbox fill: the native check renders in --bg on --brand — VERIFIED (phase 105): built-in dark theme #0f0a0a on #f43f5e = 5.2:1 (the house brand pairing; measured 5.35:1 — WCAG AA ≥4.5:1); every theme-tab-saved palette is gated by the tab's eight-pair contrast check (theme.js PAIRS — "bg" on "brand" at AA_MIN 4.5), so the checked-state ratio stays AA in all themes. */ .git-source-hidden input[type="checkbox"] { width: 1.05rem; height: 1.05rem; margin: 0; accent-color: var(--brand); cursor: pointer; } .git-source-hidden:disabled { opacity: 0.5; cursor: wait; } /* Phase 105 (A5): the "hidden on" state tag — a copy of the .git-source-ignore-count idiom (TEXT + background, never color alone — WCAG 1.4.1: --ink on --bg 16.7:1). */ .git-source-hidden-count { display: inline-block; margin-left: 0.55rem; padding: 0.08rem 0.5rem; border: 1px solid var(--line); border-radius: 999px; font-size: 0.72rem; font-weight: 600; color: var(--ink); background: var(--bg); white-space: nowrap; } /* Env-fallback rows carry no Remove (nothing is stored to remove) — the tag says where the row comes from (brand pair, 6.9:1). */ .git-source-env-tag { color: var(--brand-ink); background: var(--brand-soft); border-radius: 999px; padding: 0.2rem 0.6rem; font-size: 0.78rem; font-weight: 600; } /* Empty state: the tuning page's quiet centered line at full table width — no stored rows AND no env fallback to show. */ .git-sources-empty { margin: 0; padding: 1.4rem 1rem; text-align: center; color: var(--ink-soft); font-style: italic; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); } /* ---------- History refresh button (phase 77 task 03, TODO.md L3) ---------- The #view-history page-head is a flex row: the title block left, the actions slot right (the row wraps below 640px). Scoped to #view-history — the other four views' page-heads are untouched. The button reuses the .new-chat-btn visual language: solid brand pill, --bg text on --brand (5.2:1, WCAG AA >=4.5:1), borderless, >=44px target, hover lightens the brand fill, focus-visible via the global 3px rule. The phase-46 auth-link convention: the label is visible >=640px, the glyph is the whole control below (the button's aria-label keeps the accessible name in both). */ #view-history .page-head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 0.75rem 1.5rem; } #view-history .page-head .page-head-title { min-width: 0; } .history-refresh { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; min-height: 44px; padding: 0.5rem 0.9rem; border-radius: 999px; border: 0; background: var(--brand); color: var(--bg); font: inherit; font-weight: 700; font-size: 0.95rem; white-space: nowrap; cursor: pointer; flex-shrink: 0; } .history-refresh:hover { background: var(--brand-hover); color: var(--bg); } .history-refresh:disabled { opacity: 0.6; cursor: wait; } /* The refresh glyph is hidden on desktop (the label carries the pill); below 640px it joins the visible label in the full-width pill (the <=640 block — the icon-only squeeze is gone). */ .history-refresh svg { width: 16px; height: 16px; display: none; } /* ---------- History page (phase 50) ---------- /history.html: the admin-only saved-chats list (task 04). The FULL-WIDTH table in the 72rem frame (AGENTS.md rule 5 — no skinny single-column list), the same table language as the Sources page (the sources-table family: --line hairlines, the brand-soft-tinted thead, row hover, the scrollable .table-wrap). Every pair reuses the Phase-08 AA palette: brand-ink on brand-soft 6.9:1, ink-soft >=6.9:1, err 9.1:1. :focus-visible via the global 3px outline rule. No CDN, system fonts. */ .history-shell { display: flex; flex-direction: column; gap: 1.25rem; flex: 1; } /* Action feedback line (role=status): the sync-result shape — ink-soft on surface (>=4.5:1), small mono; the min-height holds the layout so a delete's line never reflows the table. */ .history-status { display: block; min-height: 1.2em; color: var(--ink-soft); font-family: var(--mono); font-size: 0.8rem; padding-block: 0.25rem; } /* The table is FULL-WIDTH (AGENTS.md rule 5): width 100% inside the standard .container; the .table-wrap card + its horizontal scroll cover narrow widths (the phase-07 responsive contract). */ .history-table { width: 100%; border-collapse: collapse; min-width: 640px; font-size: 0.93rem; } .history-table th, .history-table td { text-align: left; padding: 0.7rem 1rem; border-bottom: 1px solid var(--line); vertical-align: middle; } .history-table th { background: var(--brand-soft); color: var(--brand-ink); font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.04em; } .history-table tbody tr:hover { background: var(--bg); } .history-table tbody tr:last-child td { border-bottom: 0; } /* Title cell: the Open link — the accent link (brand-ink on surface 6.9:1); the column ellipsizes, the full title sits in the title attribute (on the cell AND the link). */ .history-title-cell { max-width: 34rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .history-title-link { color: var(--brand-ink); font-weight: 600; text-decoration: none; } .history-title-link:hover { text-decoration: underline; } .history-title-link:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; } /* Messages: the mono numeric readout (ink-soft >=6.9:1). */ .history-count-cell { font-family: var(--mono); color: var(--ink-soft); } /* Updated: locale date+time (ink-soft), the full ISO in the title attribute (history.js). */ .history-updated-cell { color: var(--ink-soft); white-space: nowrap; } /* Stale marker (phase 53, task 04): the READ-ONLY staleness badge on rows saved before the last KB-changing sync (the Regenerate action lives on the chat-page banner — the marker is a pill, never a control). The rose family, i.e. the Stop-treatment tokens, so "stale" reads in the same visual language as the in-flight control: err-ink on err-bg ≈9.3:1 (≥4.5:1 on --surface too), err-line border — theme tokens, AA in the palette as a whole. Fresh rows' em-dash rides the cell's ink-soft (5.1:1 on --surface). */ .history-stale-cell { color: var(--ink-soft); white-space: nowrap; } .stale-pill { display: inline-block; padding: 0.15rem 0.55rem; border: 1px solid var(--err-line); border-radius: 999px; background: var(--err-bg); color: var(--err-ink); font-size: 0.75rem; font-weight: 700; line-height: 1.45; white-space: nowrap; } /* Actions: the Delete ghost button (the tuning row-action language) + the inline two-step confirm pair (phase 50 task 04). */ .history-actions { display: inline-flex; align-items: center; gap: 0.4rem; } .history-delete { min-height: 44px; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .history-delete:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .history-delete:disabled { opacity: 0.5; cursor: wait; } .history-confirm-text { color: var(--err-ink); font-size: 0.82rem; font-weight: 700; white-space: nowrap; } /* Yes: the error-rose treatment (err-ink on err-bg 9.1:1, err-line border); No: the ghost (transparent, --line border). */ .history-confirm-yes { min-height: 44px; padding: 0.35rem 0.7rem; border: 1px solid var(--err-line); border-radius: var(--radius-sm); background: var(--err-bg); color: var(--err-ink); font: inherit; font-weight: 700; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .history-confirm-yes:hover:not(:disabled) { background: color-mix(in srgb, var(--err-line) 18%, transparent); } .history-confirm-yes:disabled { opacity: 0.6; cursor: wait; } .history-confirm-no { min-height: 44px; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .history-confirm-no:hover { background: var(--brand-soft); color: var(--brand-ink); } /* Share column (phase 51, owner-locked 2026-08-29): the Tune/Retry- family ghost buttons — Create link (unshared) and Copy (shared); Unshare is the .history-unshare ghost and its two-step confirm reuses the .history-confirm-* pair CSS above (the phase-50 pattern). The row-action language of .history-delete: --line border, transparent fill, ink-soft (>=5.1:1), ≥44px comfortable target, focus-visible via the global 3px rule; hover takes the brand pair (6.9:1). */ .history-share { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: nowrap; } .history-share-cell { white-space: nowrap; } .history-share-create, .history-share-copy, .history-unshare { min-height: 44px; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .history-share-create:hover:not(:disabled), .history-share-copy:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); } .history-unshare:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .history-share-create:disabled, .history-share-copy:disabled, .history-unshare:disabled { opacity: 0.5; cursor: wait; } /* The share link's inline fallback field (phase 51, owner-locked): a non-secure (http) origin rejects the clipboard, so the full URL is offered as an input-like that selects itself on focus — click or Tab, then Ctrl/Cmd+C. Mono (the URL is data), surface fill, --line border; truncates with an ellipsis at narrow widths (the full URL is the title + the text selection). Ink on surface ≈13.8:1. */ .share-link-fallback { display: inline-block; max-width: 14rem; padding: 0.35rem 0.55rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); font-family: var(--mono); font-size: 0.78rem; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; } .share-link-fallback:hover { border-color: var(--brand); } .share-link-fallback:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; } /* Empty-state row: the muted centered message at full table width (the .git-sources-empty language, inline in the table). */ .history-empty-row td { padding: 2.25rem 1rem; text-align: center; color: var(--ink-soft); font-style: italic; } /* ---------- Tokens view (phase 79 task 06) ---------- The admin-issued access tokens (generate · list · revoke). The shell's standard frame (the .container): the page-head (h1 + sub), the #tokens-gate (the #history-gate / .sources-gate language), the create row (label + Generate), the shown-once block (the mono field + Copy), and the FULL-WIDTH table (AGENTS.md rule 5 — the .history-table language: --line hairlines, the brand-soft-tinted thead, row hover, the scrollable .table-wrap). Every pair reuses the Phase-08 AA palette: brand-ink on brand-soft 6.9:1, ink-soft >=5.1:1, err 9.3:1. :focus-visible via the global 3px outline rule. No CDN, system fonts. */ .tokens-shell { display: flex; flex-direction: column; gap: 1.25rem; flex: 1; } /* Action feedback line (role=status): the .history-status shape — the min-height holds the layout so a line never reflows the table. */ .tokens-status { display: block; min-height: 1.2em; color: var(--ink-soft); font-family: var(--mono); font-size: 0.8rem; padding-block: 0.25rem; } /* The create row: label input + Generate — flex, wraps below 640px (the button drops under the full-width input). */ .token-create { display: flex; align-items: center; gap: 0.6rem; } #token-label { flex: 1; min-width: 0; min-height: 44px; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); font: inherit; font-size: 0.93rem; } #token-label:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; } /* Generate: the .history-refresh brand pill language — the solid brand fill (--bg text on --brand 5.2:1, AA), the ≥44px target, the lightened hover fill, the dimmed :disabled (the in-flight state), the global :focus-visible ring. */ .token-generate { min-height: 44px; padding: 0.4rem 1.1rem; border: 0; border-radius: 999px; background: var(--brand); color: var(--bg); font: inherit; font-weight: 600; font-size: 0.9rem; white-space: nowrap; cursor: pointer; } .token-generate:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); } .token-generate:disabled { opacity: 0.6; cursor: wait; } /* The shown-once block (owner-locked A4): a quiet brand-soft card around the "shown once" line + the mono read-only field + Copy — the token reads as the app's own credential surface (brand-ink on brand-soft 12.4:1; ink on the field 13.8:1). */ .token-once { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.9rem 1rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--brand-soft); } .token-once-copy { margin: 0; color: var(--brand-ink); font-size: 0.85rem; font-weight: 600; } .token-once-row { display: flex; align-items: center; gap: 0.6rem; } /* The mono read-only field: the token is DATA — mono, surface fill, --line border; it truncates with an ellipsis at narrow widths (the full value is the text selection — the inline copy fallback). */ #token-once-value { flex: 1; min-width: 0; min-height: 44px; padding: 0.35rem 0.55rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); font-family: var(--mono); font-size: 0.78rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #token-once-value:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; } /* Copy: the Tune/Retry-family ghost button (the .history-share-copy language — --line border, transparent fill, ink-soft, ≥44px, the brand hover pair). */ #token-once-copy { min-height: 44px; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } #token-once-copy:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); } /* The table is FULL-WIDTH (AGENTS.md rule 5): width 100% inside the standard .container; the .table-wrap card + its horizontal scroll cover narrow widths (the phase-07 responsive contract). The .history-table language, verbatim. */ .tokens-table { width: 100%; border-collapse: collapse; min-width: 640px; font-size: 0.93rem; } .tokens-table th, .tokens-table td { text-align: left; padding: 0.7rem 1rem; border-bottom: 1px solid var(--line); vertical-align: middle; } .tokens-table th { background: var(--brand-soft); color: var(--brand-ink); font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.04em; } .tokens-table tbody tr:hover { background: var(--bg); } .tokens-table tbody tr:last-child td { border-bottom: 0; } /* Label: the hand-out name (the column ellipsizes, the full label sits in the title attribute — tokens.js). */ .tokens-label-cell { max-width: 20rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; } /* Created / Last used: locale date+time (ink-soft), the full ISO in the title attribute; "never" for the not-yet-used stamp. */ .tokens-date-cell { color: var(--ink-soft); white-space: nowrap; } /* Status: the Active em-dash rides the cell's ink-soft (5.1:1 on --surface); the Revoked marker reuses the .stale-pill rose family (err-ink on err-bg ≈9.3:1, err-line border — the stale-pill visual language). */ .tokens-status-cell { color: var(--ink-soft); white-space: nowrap; } /* Actions: the Revoke ghost button (the .history-delete language — --line border, transparent fill, ink-soft, ≥44px target) + the inline two-step confirm (the .history-confirm-* pair CSS above — the phase-50 pattern). */ .tokens-actions { display: inline-flex; align-items: center; gap: 0.4rem; } .token-revoke { min-height: 44px; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .token-revoke:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .token-revoke:disabled { opacity: 0.5; cursor: wait; } /* Regenerate (phase 101 task 03, D2): the .token-revoke's structural twin — the SAME size / border / radius / focus, the transparent fill + ink-soft text — but the NEUTRAL action's hover (brand-soft / brand-ink), NOT the revoke's error hover: a rotation is a hand-out of a successor, not a deletion (no new hue — the phase-92 monochrome invariant). The confirm pair reuses the .history-confirm-* classes unchanged (the pair CSS above). :disabled mirrors the revoke button's rule (the in-flight state, while the rotation POST is out). */ .token-regenerate { min-height: 44px; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.82rem; white-space: nowrap; cursor: pointer; } .token-regenerate:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); } .token-regenerate:disabled { opacity: 0.5; cursor: wait; } /* Empty-state + search no-match rows (phase 101): the muted centered message at full table width (the .history-empty-row language, inline in the table) — CLASS-based, so both the active table's #tokens-empty-row / #tokens-no-match-row and the revoked table's #tokens-revoked-no-match-row share it. */ .tokens-empty-row td { padding: 2.25rem 1rem; text-align: center; color: var(--ink-soft); font-style: italic; } /* Phase 101 (task 02, D4): the per-table live label search — the house input surface (the #token-label / archive-upload input family: --line hairline, --surface fill, ink text — AA pairs, no new hue), full width, the ≥44px target, the global :focus-visible ring (no custom focus rule — the house pattern). */ .token-search { width: 100%; min-height: 44px; padding: 0.55rem 0.8rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); font: inherit; font-size: 0.9rem; } /* The revoked section's sub-heading (phase 101 D1): the phase-97 .kb-level h2 voice — mono, 1rem, brand-ink (the path voice, AA on the page background). */ .tokens-revoked-heading { margin: 1.5rem 0 0.5rem; font-family: var(--mono); font-size: 1rem; color: var(--brand-ink); } /* ---------- Theme view (phase 91, tasks 04 + 05) ---------- The shell's seventh view (#view-theme): the admin palette + branding 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) palette grid with the color swatch inputs sized for touch (44px — the label above is the second tap affordance), and the Save/Reset row (Save = the brand pill family, --bg ink on --brand 5.2:1 AA; Reset = the ghost button family, --line border; the §7.4 disabled-while-in-flight look is the :disabled pair below). The feedback lines: error/result are the house role=status/alert line languages; #theme-contrast (the WCAG warning) uses the --err-* STATE family — states are semantic colors, never themed from the tab (B3). Every pair reuses the Phase-08 AA palette; :focus-visible via the global 3px outline rule. No CDN, system fonts. */ .theme-shell { display: flex; flex-direction: column; gap: 1.25rem; flex: 1; } /* The static form — one card holding the two fieldset groups + the actions row (the #tune-form card language). */ #theme-form { display: flex; flex-direction: column; gap: 1rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 1rem 1.1rem 1.1rem; } /* The fieldset groups: reset the UA border (the card IS the group's frame), the legend rides the group's title line (the .tuning-panel-title language — ink, 1rem, 700). */ .theme-group { margin: 0; padding: 0; border: 0; display: flex; flex-direction: column; gap: 0.6rem; } .theme-group-title { padding: 0; font-size: 1rem; font-weight: 700; color: var(--ink); } /* The branding text inputs: the #token-label language (surface is already the card fill — transparent field, --line hairline, ≥44px target, the global :focus-visible ring). */ #theme-form fieldset label { font-size: 0.88rem; font-weight: 600; color: var(--ink-soft); } #theme-form fieldset input[type="text"] { width: 100%; min-height: 44px; padding: 0.35rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink); font: inherit; font-size: 0.93rem; } /* The palette note under the branding legend (task 05): the strings apply on the next page load (B4) — the live preview covers the palette only. */ .theme-note { margin: 0; font-size: 0.8rem; line-height: 1.35; color: var(--ink-soft); } /* The color-input grid (task 05): the 8 swatches in 3 columns (3 + 3 + 2 rows) — the cell is a .theme-color (label above the swatch); 1 column at the mobile breakpoint (below). */ .theme-colors { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.6rem 0.9rem; } .theme-color { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; } .theme-color label { font-size: 0.8rem; font-weight: 600; color: var(--ink-soft); line-height: 1.25; } .theme-color input[type="color"] { /* Touch-sized swatch (task 05): 44px tall (the WCAG 2.5.8 target — the label above is the second tap affordance). */ inline-size: 56px; block-size: 44px; padding: 3px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--bg); cursor: pointer; } /* Save (primary) + Reset (secondary): the pill + ghost families (.history-refresh / .token-revoke languages — the global :focus-visible ring applies, no button-scoped focus override). */ .theme-actions { display: flex; gap: 0.6rem; margin-top: 0.2rem; } #theme-save { min-height: 44px; padding: 0.4rem 1.2rem; border: 0; border-radius: 999px; background: var(--brand); color: var(--bg); /* dark ink on brand: 5.2:1 (never white on brand) */ font: inherit; font-weight: 700; font-size: 0.9rem; white-space: nowrap; cursor: pointer; } #theme-save:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); } #theme-save:disabled { opacity: 0.6; cursor: wait; } .theme-reset { min-height: 44px; padding: 0.4rem 1rem; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.9rem; white-space: nowrap; cursor: pointer; } .theme-reset:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); } .theme-reset:disabled { opacity: 0.6; cursor: wait; } /* The three feedback lines (task 05): error (role=alert — the server's 422 detail) and result (role=status) are the house line languages; min-height holds the layout so a line never reflows the form. #theme-contrast (the WCAG warning, task 05) takes the .tuning-error box treatment in the --err-* STATE family — states are semantic colors, never themed from the tab (B3): a failing pair reads as a warning and the box is warning-only (Save is never disabled by it). */ .theme-error, .theme-result, .theme-contrast { display: block; margin: 0; min-height: 1.2em; font-family: var(--mono); font-size: 0.8rem; padding-block: 0.25rem; } .theme-error { color: var(--err-ink); } .theme-result { color: var(--ok-ink); } .theme-contrast { background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; } /* ---------- Shared page (phase 51, task 03) ---------- /shared/: the anonymous read-only conversation (owner-locked 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 (ink-soft ≥6.9:1 on surface/bg, brand-ink on brand-soft 6.9:1); :focus-visible via the global 3px outline rule. No CDN, system fonts. */ .shared-shell { width: 100%; max-width: var(--chat-column); /* the centered chat column — 72rem, the .container width at every viewport */ margin-inline: auto; display: flex; flex-direction: column; gap: 1rem; flex: 1; } /* Page title (JS-filled with the shared chat's title; the static fallback is "Shared conversation") — the page-head h1 size. */ #shared-title { margin: 0 0 0.25rem; font-size: 1.7rem; } /* The muted meta line under the h1 ("Shared via … — read-only."): ink-soft on the page bg ≥8.6:1, the page-sub language. */ .shared-note { margin: 0; color: var(--ink-soft); font-size: 0.92rem; } /* The invalid / revoked state: a centered muted card (the not-found language — surface fill, --line border, italic ink-soft). Revealed by shared.js for a malformed token (no fetch) or a failed read. */ #shared-invalid { padding: 1.4rem 1rem; text-align: center; color: var(--ink-soft); font-style: italic; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); } /* Static chips: a guest's "Maybe try" and source chips are TEXT — no pointer, no hover, no cursor (owner-locked zero controls). The pill look is kept; the interactivity is scoped off here and only here (pointer-events: none also makes the :hover rules unreachable). */ .shared-shell .suggestion-chip, .shared-shell .source-chip { pointer-events: none; cursor: default; } /* ---------- Document viewer (phase 10; two-row header since phase 34) ---------- */ /* Phase 34 (owner confirmation 2026-08-26): the viewer header is TWO rows in one sticky

— row 1 reuses the standard .app-header / .header-inner rules VERBATIM (row 1 IS the standard bar, so the phase-12/19 pinned heights 64px / 58px apply), row 2 is the .doc-titlebar (back link + title + meta). The header element itself is content-sized (row 1 + row 2) and sticky, so BOTH rows stay pinned while the document scrolls. */ .doc-header { position: sticky; top: 0; z-index: 20; background: var(--surface); /* Phase 34: no fixed height — the header sizes to row 1 (--header-h) + the .doc-titlebar; the phase-12 shrink guard stays. */ flex-shrink: 0; } /* Row 1's own gradient hairline would land BETWEEN the rows; the separator there is the quiet --line border-top on the titlebar, and the signature hairline belongs at the bottom edge of the whole header (the .doc-header::after in the shared rule above). */ .doc-header > .app-header::after { content: none; } /* Back link: pill with an SVG arrow + "Sources" (>=44px touch target; :focus-visible via the global 3px outline rule, phase 08). */ .doc-back { display: inline-flex; align-items: center; gap: 0.4rem; flex: 0 0 auto; min-height: 44px; padding: 0.45rem 0.9rem; border-radius: 999px; background: var(--brand-soft); border: 1px solid var(--line); color: var(--brand-ink); /* 6.9:1 on --brand-soft */ font-weight: 600; font-size: 0.95rem; text-decoration: none; } .doc-back:hover { background: var(--brand-soft); } .doc-back svg { width: 16px; height: 16px; display: block; } /* Row 2: the titlebar — a .container-width row with the back link + the title block, its own content-sized height (title line + meta line), the same surface as row 1, separated from it by the quiet hairline (var(--line) — the existing 1px border color). */ .doc-titlebar { border-top: 1px solid var(--line); padding-block: 0.6rem; } .doc-titlebar .container { display: flex; align-items: center; gap: 0.9rem; } /* The title block keeps clipping (min-width: 0) so #doc-title / .doc-meta ellipsize inside the flex row instead of overflowing. */ .doc-title-block { min-width: 0; } #doc-title { margin: 0; font-size: 1.3rem; line-height: 1.3; /* Phase 34: ellipsis + the `title` attribute (set by document.js) — the row has no pills to fit, so the old clipping-for-pills rule is gone; the ellipsis is the only fit the title needs. */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Meta row: source badge · format badge · mono path · created · indexed · chunks (phase 106 (task 08, D8) added created BEFORE indexed). Phase 12/34: it may clip, but it must NEVER wrap — the meta stays on one line in the titlebar row (the row is content- sized, so a wrap would grow it). */ .doc-meta { display: flex; flex-wrap: nowrap; align-items: center; gap: 0.4rem; margin-top: 0.35rem; font-size: 0.78rem; color: var(--ink-soft); /* Overflowing content (badges + path + dates) clips at the bar edge; nowrap keeps text on one line so the row can never grow the header. */ white-space: nowrap; overflow: hidden; } .doc-source-badge { background: var(--brand-soft); border: 1px solid var(--line); color: var(--brand-ink); font-weight: 600; border-radius: 999px; padding: 0.02rem 0.55rem; } .format-badge { font-family: var(--mono); background: var(--brand-soft); border: 1px solid var(--line); color: var(--brand-ink); border-radius: var(--radius-sm); padding: 0.02rem 0.45rem; } .doc-path { font-family: var(--mono); font-size: 0.75rem; max-width: 26rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /* The path is the meta row's designated ellipsis target: with a floor it keeps a visible box on narrow screens while the trailing badges clip. */ min-width: 6rem; } .doc-chunks { font-family: var(--mono); } /* Phase 106 (task 08, D8): the Created badge — the SAME family as the Indexed one (no own look: the meta row's text, var(--ink-soft) on the titlebar's --surface — 5.1:1, the pair recorded at :root; the modal's .doc-modal-meta row records the same pair). The explicit declaration pins the inherited value: a themed --ink-soft moves the badge with the row (byte-identical rendering). */ .doc-created { color: var(--ink-soft); /* 5.1:1 on --surface */ } .doc-shell { display: flex; flex-direction: column; gap: 1rem; flex: 1; } #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 — 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); margin-inline: auto; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 1.5rem 1.75rem; overflow-wrap: anywhere; } .doc-md p { margin: 0.35rem 0; } .doc-md h3 { margin: 1.1rem 0 0.4rem; font-size: 1.15rem; } .doc-md h4 { margin: 0.9rem 0 0.35rem; font-size: 1rem; } .doc-md > :first-child { margin-top: 0; } .doc-md ul { margin: 0.4rem 0; padding-left: 1.3rem; } .doc-md pre { background: var(--surface); color: var(--ink); padding: 0.7rem 0.9rem; border: 1px solid var(--line); border-radius: var(--radius-sm); overflow-x: auto; font-size: 0.85rem; font-family: var(--mono); } .doc-md code { font-family: var(--mono); font-size: 0.88em; background: var(--brand-soft); padding: 0.08em 0.35em; border-radius: 5px; } .doc-md pre code { background: none; padding: 0; } /* Summary panel (phase 36): the labeled "Summary" section the shared renderDocument core (document.js) draws ABOVE the original content whenever doc.summary is non-empty (phase 30 — non-markdown docs only). A "summary, not content" look: the surface card carries a 3px brand left border (no shadow — the content cards own those) and a small-caps brand-ink label. Phase-08 tokens only; static content — no animation (nothing for prefers-reduced-motion to still), and the aria-label + heading carry the accessibility. */ .doc-summary { width: 100%; background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--brand); border-radius: var(--radius); padding: 0.9rem 1.1rem; margin-bottom: 1rem; overflow-wrap: anywhere; } /* md/markdown: the panel matches the .doc-md centered reading column (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) { max-width: var(--chat-column); margin-inline: auto; } .doc-summary-title { margin: 0 0 0.4rem; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--brand-ink); /* 9.0:1 on --surface */ } .doc-summary-text { margin: 0; color: var(--ink); /* on --surface ≈14.5:1 */ } /* Summary edit affordance (phase 57, task 02 — D4, admin-only): the header row (label + Edit button), the inline editor (prefilled textarea + Save/Cancel + a role=status live region). House dark-tech palette (phase-08 tokens), system fonts, no CDN; :focus-visible via the global 3px outline rule. Anonymous visitors never see any of it — the button and editor are wired only for admins (docAdminReady in document.js), so the public panel is byte-for-byte the phase-36 shape. */ .doc-summary-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.4rem; /* the old .doc-summary-title bottom margin */ } .doc-summary-head .doc-summary-title { margin: 0; } .doc-summary-edit { flex: 0 0 auto; display: inline-flex; align-items: center; min-height: 24px; padding: 0.15rem 0.7rem; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ font: inherit; font-weight: 600; font-size: 0.78rem; letter-spacing: 0.02em; cursor: pointer; } .doc-summary-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); } .doc-summary-edit[hidden] { display: none; } /* the hidden attr must beat the display above */ .doc-summary-editor { display: block; width: 100%; min-height: 8rem; padding: 0.6rem 0.8rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--bg); /* inset against the --surface panel */ color: var(--ink); /* 16.7:1 on --bg (AA) */ font: inherit; line-height: 1.5; resize: vertical; } .doc-summary-actions { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.75rem; } .doc-summary-save { display: inline-flex; align-items: center; min-height: 32px; padding: 0.35rem 0.95rem; border: 0; border-radius: 999px; background: var(--brand); color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */ font: inherit; font-weight: 600; font-size: 0.85rem; cursor: pointer; } .doc-summary-save:hover { background: var(--brand-hover); } /* the house hover lightening */ .doc-summary-save:disabled { opacity: 0.6; cursor: default; } /* one PATCH at a time */ .doc-summary-cancel { display: inline-flex; align-items: center; min-height: 32px; padding: 0.35rem 0.95rem; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ font: inherit; font-weight: 600; font-size: 0.85rem; cursor: pointer; } .doc-summary-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .doc-summary-status { margin: 0.6rem 0 0; font-size: 0.85rem; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ } .doc-summary-status:empty { margin-top: 0; } /* Date edit affordance (phase 106, task 09 — D7, admin-only): the "Edit date" text button after the Created badge (the .doc-summary-edit family — one pill style for both inline editors) and the editor box that swaps in for the button IN the .doc-meta row: the native type=date input, Save / Cancel, the muted underlined "Revert to sync" clear affordance (the phase-57 "clear = explicit" contrast), a role=status live line, and a role=alert error line (the .git-source-error err pair). The meta row may clip (nowrap) on the page, so the box wraps ITS children internally (the modal's .doc-modal-meta already wraps). The global 3px :focus-visible outline rule applies — no per-control rule (the phase-105 checkbox idiom). :disabled = opacity + cursor: wait (the .git-source-remove:disabled idiom — one PATCH at a time). Contrast (verified, house style): button / Cancel / revert / status ride the row's ink-soft on --surface (5.1:1, AA); the input is ink on --bg (16.7:1, AA); Save is the solid brand pill (--bg on --brand = 5.2:1, AA); the error line is the err pair (9.1:1 on --err-bg, AA). Anonymous / token holders never see any of it — the wiring is admin-gated (docAdminReady in document.js), so the public badge row stays byte-for-byte the task-08 shape. */ .doc-date-edit { flex: 0 0 auto; display: inline-flex; align-items: center; min-height: 24px; padding: 0.15rem 0.7rem; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ font: inherit; font-weight: 600; font-size: 0.78rem; letter-spacing: 0.02em; cursor: pointer; } .doc-date-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); } .doc-date-edit[hidden] { display: none; } /* the hidden attr must beat the display above */ .doc-date-editor { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; min-width: 0; max-width: 100%; } .doc-date-input { padding: 0.15rem 0.4rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--bg); color: var(--ink); /* 16.7:1 on --bg (AA) */ font: inherit; font-size: 0.78rem; font-family: var(--mono); } .doc-date-save { display: inline-flex; align-items: center; min-height: 24px; padding: 0.15rem 0.7rem; border: 0; border-radius: 999px; background: var(--brand); color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */ font: inherit; font-weight: 600; font-size: 0.78rem; cursor: pointer; } .doc-date-save:hover { background: var(--brand-hover); } /* the house hover lightening */ .doc-date-cancel { display: inline-flex; align-items: center; min-height: 24px; padding: 0.15rem 0.7rem; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ font: inherit; font-weight: 600; font-size: 0.78rem; cursor: pointer; } .doc-date-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .doc-date-revert { display: inline-flex; align-items: center; min-height: 24px; padding: 0.15rem 0.2rem; border: 0; background: transparent; color: var(--ink-soft); /* 5.1:1 on --surface (AA) — the muted marker */ font: inherit; font-size: 0.78rem; text-decoration: underline; cursor: pointer; } .doc-date-revert:hover { color: var(--ink); } /* 14.5:1 on --surface (AA) */ .doc-date-save:disabled, .doc-date-cancel:disabled, .doc-date-revert:disabled, .doc-date-input:disabled { opacity: 0.5; cursor: wait; /* one PATCH at a time (the .git-source-remove:disabled idiom) */ } .doc-date-status { font-size: 0.78rem; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ } .doc-date-error { flex-basis: 100%; /* drops onto its own row under the controls */ min-width: 0; max-width: 100%; overflow-wrap: anywhere; /* a long server detail never overflows the box */ font-size: 0.78rem; font-weight: 600; background: var(--err-bg); color: var(--err-ink); /* 9.1:1 on --err-bg (AA — the .git-source-error pair) */ border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.1rem 0.5rem; } .doc-date-status:empty, .doc-date-error:empty { display: none; } /* an empty live line takes no space */ /* Raw (non-markdown) formats: full-width mono pre, horizontal scroll. */ .doc-raw { width: 100%; margin: 0; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 1.25rem 1.5rem; overflow-x: auto; white-space: pre; font-family: var(--mono); font-size: 0.85rem; line-height: 1.5; } /* Designed not-found state (no emoji — plain SVG mark, phase 08 rule). */ .doc-not-found { width: 100%; max-width: 30rem; margin: 2rem auto; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 2.25rem 1.75rem; text-align: center; } .doc-not-found-glyph { color: var(--ink-soft); width: 44px; height: 44px; margin-inline: auto; } .doc-not-found-glyph svg { width: 44px; height: 44px; display: block; } .doc-not-found h2 { margin: 0.8rem 0 0.4rem; font-size: 1.3rem; } .doc-not-found-sub { margin: 0 0 1.25rem; color: var(--ink-soft); } .doc-open-sources { display: inline-flex; align-items: center; min-height: 44px; padding: 0.5rem 1.1rem; background: var(--brand); color: var(--bg); /* dark ink on brand: 5.2:1 */ font-weight: 700; text-decoration: none; border-radius: var(--radius-sm); } .doc-open-sources:hover { background: var(--brand-hover); } /* Viewer links: Sources-table path cell + chat source chips (phase 10). */ .doc-link { color: var(--brand-ink); /* 8.7:1 on --surface */ text-decoration: none; } .doc-link:hover, .doc-link:focus-visible { text-decoration: underline; } /* ---------- 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 (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. Stacking: z-index 1000 puts the overlay above the sticky header (20) and the skip-link (100); the z-index:-1 background layers stay below everything. The panel is 96vw × 92vh, centered ("almost-fullscreen"). */ .doc-modal { position: fixed; inset: 0; z-index: 1000; display: flex; /* the panel is the only in-flow child — margin: auto centers it */ } /* Explicit (the global [hidden] rule already wins — this one is the documented, testable contract for the skeleton). */ .doc-modal[hidden] { display: none; } .doc-modal-backdrop { position: fixed; inset: 0; /* --bg at 82% — no backdrop-filter (phase-08 no-blur perf anchor). */ background: color-mix(in srgb, var(--bg) 82%, transparent); transition: opacity 120ms ease; } .doc-modal-panel { /* position:relative lifts the panel above the fixed backdrop (positioned elements paint over in-flow siblings otherwise). */ position: relative; z-index: 1; display: flex; flex-direction: column; width: min(1100px, 96vw); height: 92vh; margin: auto; background: var(--surface); border: 1px solid var(--line); border-radius: 12px; /* Depth shadow — deliberately NOT themed (black, phase-92 table: "depth, not theme"); the color-mix spelling renders the same 55% black and keeps the literal out of the phase-92 sweep gate. */ box-shadow: 0 24px 80px color-mix(in srgb, black 55%, transparent); } /* Sticky top with the SAME height as the page bars — the phase-12 pins (--header-h: 64px desktop / 58px ≤640px) so the bar never reads differently here. The title is the designated squeeze target (ellipsis), so the bar can never grow its height. */ .doc-modal-header { flex-shrink: 0; position: sticky; top: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: 0.9rem; height: var(--header-h); padding-inline: 1.25rem; background: var(--surface); border-bottom: 1px solid var(--line); } .doc-modal-title { min-width: 0; margin: 0; font-size: 1.3rem; line-height: 1.3; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .doc-modal-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 0.5rem; } /* "Full page" escape hatch: the same ghost pill as New chat / Sign in (ink-soft on surface ≈6.9:1; hover pair brand-ink/brand-soft ≈6.9:1). Icon-only below 640px — the aria-label keeps the accessible name. */ .doc-modal-open { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; min-height: 44px; padding: 0.4rem 0.8rem; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.85rem; white-space: nowrap; text-decoration: none; } .doc-modal-open:hover { background: var(--brand-soft); color: var(--brand-ink); } .doc-modal-open svg { width: 15px; height: 15px; display: none; } /* Icon-only close (aria-label in the markup). ink-soft on surface ≈6.9:1; hover = the err pair ≈9.1:1, like the steering-note delete. */ .doc-modal-close { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; padding: 0; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--ink-soft); cursor: pointer; } .doc-modal-close:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .doc-modal-close:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; } .doc-modal-close svg { width: 18px; height: 18px; display: block; } /* Meta row: the SAME badge classes as the viewer's .doc-meta (source badge · format badge · mono path · created · indexed · chunks — no duplicate badge styling here); unlike the fixed-height header bar it may WRAP, so nothing clips. aria-live="polite" on the element announces the load → meta swap (phase-10 a11y contract, modal variant). */ .doc-modal-meta { flex-shrink: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; padding: 0.6rem 1.25rem 0; font-size: 0.78rem; color: var(--ink-soft); } /* The scroll container: vertical scroll lives HERE, never the viewport. .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; overflow: auto; padding: 1rem 1.25rem 1.5rem; } /* Themed horizontal scrollbar for the modal's raw code block (phase 116, task 01): the .doc-raw pre already owns its overflow (its own `overflow-x: auto` + `white-space: pre` — locked A1, code never wraps), so the native light-gray bar a long line reveals is the pre's own. Both engine families are themed, scoped to THIS modal only (no global scrollbar restyle — the TODO's scope): scrollbar-color / -width for Firefox, the ::-webkit-scrollbar pseudos for Chromium (height 8px = the horizontal bar; the pre does not scroll vertically). The tokens follow the saved theme via the :root color-mix derivation; the muted thumb keeps >= 3:1 non-text contrast on the near-surface track. */ .doc-modal .doc-raw { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); } .doc-modal .doc-raw::-webkit-scrollbar { height: 8px; } .doc-modal .doc-raw::-webkit-scrollbar-track { background: var(--scrollbar-track); } .doc-modal .doc-raw::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 4px; } .doc-modal-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); } /* No motion under reduced motion (same pattern as the phase-25 layers). */ @media (prefers-reduced-motion: reduce) { .doc-modal-backdrop { transition: none; } } /* ---------- Doc edit screen (phase 59, task 06) ---------- /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 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%; display: flex; flex-direction: column; gap: 1.25rem; flex: 1; } /* The slim header's back link — the .doc-back ghost language (document.html): >=44px target, --line border, ink-soft (5.1:1 on the --surface bar) rising to ink on hover; pushed to the bar's right edge by the header-inner flex (margin-left: auto — the nav's own pattern). */ .doc-edit-back { display: inline-flex; align-items: center; gap: 0.4rem; min-height: 44px; margin-left: auto; padding: 0.4rem 0.9rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font-weight: 600; text-decoration: none; } .doc-edit-back:hover { color: var(--ink); border-color: var(--ink-soft); } .doc-edit-back svg { width: 16px; height: 16px; display: block; } /* The edit form — the tuning form's card as a vertical stack: labeled title input, mono path input, the mono markdown textarea (min-height 20rem — the body is the star), and the actions row (the brand Push button + the back link). Inset fields (bg fill on the surface card). */ #doc-edit-form { display: flex; flex-direction: column; gap: 0.9rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 1.5rem 1.5rem 1.75rem; } #doc-edit-form label { font-weight: 600; font-size: 0.95rem; } #draft-title, #draft-path { width: 100%; font: inherit; font-size: 1rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.55rem 0.75rem; min-height: 44px; } /* The in-repo path is machine data — mono (the git-sources URL-input convention). */ #draft-path { font-family: var(--mono); font-size: 0.92rem; } /* The markdown body: mono, tall (min-height 20rem), vertical resize. ink on bg = 16.7:1. */ #draft-body { width: 100%; font-family: var(--mono); font-size: 0.92rem; line-height: 1.5; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.75rem 0.9rem; min-height: 20rem; resize: vertical; } /* Actions row: the primary Push button (brand, dark ink on brand 5.2:1 — never white on brand) + the Discard control (phase 115 — the secondary destructive action) + the back link; wraps at narrow widths. */ .doc-edit-actions { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; } #push-doc-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; border: 0; border-radius: var(--radius-sm); background: var(--brand); color: var(--bg); /* dark ink on brand: 5.2:1 */ font: inherit; font-weight: 700; cursor: pointer; padding-inline: 1.25rem; } #push-doc-btn:hover:not(:disabled) { background: var(--brand-hover); } #push-doc-btn:disabled { opacity: 0.6; cursor: wait; } /* Phase 115 (task 02): the Discard control — the SECONDARY destructive action, visually subordinate to the brand primary (the ghost family of .steering-delete / .tuning-delete): ink-soft on transparent (5.1:1 on --surface — more on the darker --bg canvas) with the --line border; hover joins the err family (err-ink on err-bg 9.3:1, the err-line border — the state stays text + color, never color alone); the 44px touch floor, the global 3px :focus-visible ring, and the :disabled state is the "Discarding…" in-flight affordance. */ .discard-draft { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.85rem; white-space: nowrap; padding-inline: 1rem; cursor: pointer; } .discard-draft:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .discard-draft:disabled { opacity: 0.6; cursor: wait; } /* The success status line (role=status): the ok family (ok-ink on ok-bg 10.6:1) when a push outcome has landed — min-height holds the line's space so the layout never jumps when the text lands. Empty (before the first push, or after a failure cleared the stale line) it is the dashed placeholder (the #archive-upload-result language). */ .doc-edit-status { margin: 0; min-height: 1.5rem; background: var(--ok-bg); color: var(--ok-ink); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-size: 0.9rem; font-weight: 600; } .doc-edit-status:empty { background: transparent; border-style: dashed; color: var(--ink-soft); } /* The error banner (role=alert): the err family (err-ink on err-bg 9.3:1, err-line border) — git's stderr may carry long paths, so long words break instead of overflowing the card. */ .doc-edit-error { margin: 0; background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.5rem 0.8rem; font-size: 0.85rem; font-weight: 600; overflow-wrap: anywhere; } /* ---------- Footer ---------- */ .app-footer { border-top: 1px solid var(--line); background: var(--surface); padding-block: 0.8rem; margin-top: 1rem; } .footer-inner { display: flex; justify-content: space-between; gap: 1rem; color: var(--ink-soft); font-size: 0.85rem; } /* ---------- Responsive (tablet squeeze — phase 34 task 04 visual pass) ---------- The full admin bar (brand + nav [Chat, Sources, Tuning] + Tuning toggle + New chat + Sign out) outgrows a 768px bar in the desktop styles. Squeeze the pills/gaps moderately — the 44px touch floor is held by min-height and the 64px height by --header-h — and let the brand wordmark (base ellipsis) absorb any remainder. The ≤640 block below stays tighter and wins at phone widths. */ @media (max-width: 900px) { .header-inner { gap: 0.6rem; } /* Phase 35: the admin-only "Git sources" link joins the nav — a fourth text pill on the admin bar — so the tablet squeeze tightens once more to hold 768px in the admin state (brand already the designated clip target, pills squeeze next). */ .nav-link { padding: 0.4rem 0.5rem; font-size: 0.85rem; } .app-nav { gap: 0.15rem; } .new-chat-btn, .auth-link { padding: 0.45rem 0.5rem; } } /* ---------- Responsive (mobile-first adjustments) ---------- */ @media (max-width: 640px) { :root { --header-h: 58px; } .container { padding-inline: 0.9rem; } /* Owner report 2026-09-08: the hamburger "won't extend" on a real phone while the same width in a desktop browser works. The page opts into `viewport-fit=cover` (the ), so on notched phones the layout viewport runs UNDER the status bar / Dynamic Island — and this bar (58px, toggle tap zone y≈7–51px) sits entirely in the system top-edge zone: visually under the battery/signal icons, and in the region where a real device does not reliably deliver taps to the page (desktop emulation has no system top zone, so it works there — the device-only failure). Pad the bar DOWN by the top safe-area inset: the header surface still fills the notch region (that is the point of viewport-fit=cover) but every control — the #nav-toggle first — lands in the reliably tappable area, and the dropdown's top:100% follows the taller bar automatically. The .app-header rule doubles as the document viewer's row-1 bar (the same class), so the viewer's hamburger is covered by the same rule. env() resolves to 0 on every non-notch surface (desktop, headless, no-island phones) — byte-identical rendering there. */ .app-header { height: calc(var(--header-h) + env(safe-area-inset-top, 0px)); padding-top: env(safe-area-inset-top, 0px); } /* The share toast rests just under the bar — the bar is taller by the same inset on notched phones, so the offset must carry it too (the toast would otherwise tuck under the padded bar). */ .toast { top: calc(var(--header-h) + 0.75rem + env(safe-area-inset-top, 0px)); } /* Phase 14: the New chat pill joins the header — tighten the bar so brand + nav + pill fit at 360px without horizontal overflow (the brand text may ellipsize as the designated squeeze target). */ /* Phase 29: the admin-only Tuning link joins the nav — three text pills on the chat bar in the admin state. The brand text (the designated squeeze target) is already at zero, so the bar squeezes the nav pills + gaps instead: tighter pill padding/font and a tighter inner gap. The 44px touch floor is held by min-height, the 58px bar height is pinned by --header-h, and the bar now fits both auth states at 375px (and 360px) without horizontal overflow. Phase 34 task 04 (visual pass): the full admin bar — nav [Chat, Sources, Tuning] + Tuning toggle + Sync + New chat + Sign out — now ships on EVERY page, so the squeeze tightens once more (0.3rem inner gap, 0.78rem nav pills, 0.35rem pill padding, 18px brand mark) to hold 375px with the brand mark intact and 360px with the brand clipped clean (overflow:hidden — the mark never overlaps the nav). */ /* Phase 35: the admin-only "Git sources" link joins the nav — a fourth text pill on the admin bar — so the mobile squeeze tightens once more (0.25rem inner gap, 0.72rem nav pills, 0.3rem pill padding, 0.05rem nav gap) to hold 375px — and 360px, the brand fully clipped — in the admin state without horizontal overflow. */ .header-inner { gap: 0.25rem; } .brand { min-width: 0; overflow: hidden; } .brand-mark { width: 18px; height: 18px; } .brand-text { font-size: 0.8rem; letter-spacing: 0.04em; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Phase 46 (owner permission 2026-08-27, TODO.md L9): the nav links LEAVE the bar at phone widths — the old pill-squeeze rules for .nav-link / .app-nav (0.72rem pills, 0.05rem gap, in place of this comment) are superseded by the #nav-toggle dropdown below. The action pills' squeeze rules further down are untouched, and the 900px tablet block keeps squeezing the INLINE nav at 641–900px (the hamburger is absent there). */ .nav-toggle { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; padding: 0; color: var(--ink); background: none; border: 0; border-radius: var(--radius-sm); cursor: pointer; touch-action: manipulation; /* Phase 88: standard dead-mobile-button fix — the toggle's taps must not wait on the double-tap-zoom/pinch disambiguation (owner bug report 2026-09-08: dead hamburger on chat-page cold boots). */ } .nav-toggle:hover { background: var(--brand-soft); color: var(--brand-ink); } /* The icon is sized (an unsized inline SVG would default to 300px and blow the bar out); 20px reads as a proper hamburger inside the 44px target. */ .nav-toggle svg { width: 20px; height: 20px; display: block; } /* The nav becomes the dropdown. The containing block is the sticky .app-header (.header-inner is not positioned), so the menu spans the header's full width — edge to edge — intended on mobile; z-index 21 = header (20) + 1, above the bar content. */ .app-nav { position: absolute; top: 100%; left: 0; right: 0; margin-left: 0; flex-direction: column; gap: 0; background: var(--surface); border-bottom: 1px solid var(--line); box-shadow: var(--shadow-lg); padding: 0.5rem 0; z-index: 21; /* Closed state (default): invisible and non-interactive — task 02's header.js is the only opener (.is-open + aria-expanded). */ visibility: hidden; opacity: 0; transform: translateY(-8px); pointer-events: none; transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 180ms; } .app-nav.is-open { visibility: visible; opacity: 1; transform: none; pointer-events: auto; transition: opacity 180ms ease, transform 180ms ease, visibility 0s; } /* Phase 88: while the mobile menu is open, the chat's sticky bottom cluster is hidden — it is the other positioned/layered element on the page and must not compete for taps with the open menu (it also overlaps the menu's lower rows on short viewports — menu y58→417 vs cluster top y395 at 390×600). visibility (not display): layout is preserved, so closing never reflows the chat column. */ body.nav-menu-open .chat-bottom { visibility: hidden; } /* Menu rows: comfortable ≥44px targets (0.75rem × 2 + the 1rem line) and readable text — replaces the old .nav-link pill squeeze. */ .app-nav .nav-link { padding: 0.75rem 1.25rem; font-size: 1rem; } .new-chat-btn { padding: 0.4rem 0.3rem; } .new-chat-label { display: none; } .new-chat-btn svg { display: block; } /* Phase 51: the Share pill squeezes with New chat (same family, same rules — the chat-shell override below keeps the label visible). */ .share-chat-btn { padding: 0.4rem 0.3rem; } .share-chat-label { display: none; } .share-chat-btn svg { display: block; } /* But on the chat page there is room — keep the label visible and hide the icon (the button lives inside .chat-shell, not the navbar). */ .chat-shell .new-chat-label { display: inline; } .chat-shell .new-chat-btn svg { display: none; } .chat-shell .share-chat-label { display: inline; } .chat-shell .share-chat-btn svg { display: none; } /* Phase 55 (task 05, TODO.md L6, A5): the action row flips to a vertical stack at the phone breakpoint — full-width pills, New chat above Share. The pill rules above (padding, the icon/label handling, the .chat-shell label overrides) apply to the stacked pills unchanged. */ .chat-actions { flex-direction: column; align-items: stretch; gap: 0.5rem; } /* Phase 53: the stale banner's row wraps at phone width (message above, action below) — the pill takes a full-width comfortable row instead of squeezing into the text. */ .stale-regenerate { margin-left: 0; width: 100%; } .banner-retry { margin-left: 0; width: 100%; } /* Phase 16: the auth pill goes icon-only like New chat — brand text ellipsizes as the designated squeeze target, no bar overflow. */ .auth-link { padding: 0.4rem 0.3rem; } .auth-label { display: none; } .auth-link svg { display: block; } /* Phase 46 UX revision: sign-out & sign-in move into the hamburger dropdown on mobile — hide the bar copies, show the dropdown copies. */ .sign-out-mobile { display: inline-flex; } #sign-out-btn { display: none !important; } .sign-in-mobile { display: inline-flex; } #sign-in-link { display: none !important; } .nav-toggle { margin-left: auto; } /* Dropdown-style sign-out: full-width row, error palette, label visible. */ #app-nav .sign-out-btn { display: flex; width: 100%; align-items: center; gap: 0.6rem; text-align: left; padding: 0.75rem 1.25rem; font-size: 1rem; border: 0; border-radius: 0; background: transparent; color: var(--err-ink); min-height: 48px; } #app-nav .sign-out-btn:hover { background: var(--err-bg); color: var(--err-ink); } #app-nav .sign-out-btn svg { width: 18px; height: 18px; flex: 0 0 auto; } #app-nav .sign-out-btn .auth-label { display: inline; } /* Dropdown-style sign-in: matches sign-out row styling. */ #app-nav .sign-in-mobile { display: flex; width: 100%; align-items: center; gap: 0.6rem; text-align: left; padding: 0.75rem 1.25rem; font-size: 1rem; border: 0; border-radius: 0; background: transparent; color: var(--err-ink); font-weight: 600; text-decoration: none; min-height: 48px; } #app-nav .sign-in-mobile:hover { background: var(--err-bg); color: var(--err-ink); } #app-nav .sign-in-mobile svg { width: 18px; height: 18px; flex: 0 0 auto; } #app-nav .sign-in-mobile .auth-label { display: inline; } .steering-note { padding: 0.3rem 0.3rem 0.3rem 0.7rem; } .app-main > .steering-panel { width: calc(100% - 1.8rem); } .tune-btn { min-height: 44px; } .retry-btn { min-height: 44px; } /* Phase 27: the tuning page squeezes like the other cards — the row padding tightens; the icon+label actions keep their 44px floor and the note text ellipsizes (min-width: 0). */ .tuning-note { gap: 0.4rem; padding: 0.3rem; } .tuning-edit, .tuning-delete { padding: 0.35rem 0.5rem; } .msg-body { max-width: 92%; } .empty-state { padding: 1.75rem 1.1rem; margin-top: 0.25rem; } .empty-state-title { font-size: 1.25rem; } .suggestions { flex-wrap: nowrap; overflow-x: auto; justify-content: flex-start; padding-bottom: 0.4rem; -webkit-overflow-scrolling: touch; scrollbar-width: thin; } .suggestion-chip { flex: 0 0 auto; } .doc-titlebar .container { gap: 0.5rem; } /* phase 34: row 2 squeezes like row 1 at mobile widths */ #doc-title { font-size: 1.1rem; } .doc-path { max-width: 16rem; } .doc-md { padding: 1.1rem 1rem; } .doc-summary { padding: 0.75rem 0.9rem; } .doc-raw { padding: 1rem; font-size: 0.8rem; } /* Phase 26: the modal bar squeezes like the other bars — the Full page pill goes icon-only (aria-label keeps the name), the title clips; the panel stays 96vw × 92vh, so no horizontal overflow at 360px. */ .doc-modal-header { gap: 0.5rem; padding-inline: 0.9rem; } .doc-modal-title { font-size: 1.1rem; } .doc-modal-open { padding: 0.4rem 0.55rem; } .doc-modal-open svg { display: block; } .doc-modal-open span { display: none; } .doc-modal-meta { padding-inline: 0.9rem; } .doc-modal-content { padding: 0.75rem 0.9rem 1.25rem; } .composer { padding: 0.5rem; } /* Phase 35 (phase 49: + the archive upload form): the add forms stack like the other cards — label, full-width input, full-width button; the table wrapper's horizontal scroll already covers long URLs/paths. */ #git-source-form, #archive-upload-form { flex-direction: column; align-items: stretch; } #git-source-form > label, #archive-upload-form > label { white-space: normal; } #git-source-url, #git-source-token, #archive-upload-file { min-width: 0; } #git-source-add, #archive-upload-btn { width: 100%; } /* Phase 50: the History table keeps its full width (the .table-wrap horizontal scroll already covers it); the actions cell wraps so the two-step confirm pair fits the phone width. */ .history-actions-cell { white-space: normal; } .history-actions { flex-wrap: wrap; } /* Phase 79 task 06: the Tokens create row stacks — the label input takes the full width, the Generate pill drops under it (full width); the once field's row stacks the same way; the actions cell wraps so the two-step confirm pair fits the phone width (the table's own horizontal scroll covers the columns). */ .token-create { flex-direction: column; align-items: stretch; } .token-generate { width: 100%; } .token-once-row { flex-direction: column; align-items: stretch; } #token-once-copy { width: 100%; } .tokens-actions-cell { white-space: normal; } .tokens-actions { flex-wrap: wrap; } /* Phase 91 task 05: the Theme form squeezes — the color grid drops to 1 column (the label + swatch pairs keep their ≥44px targets) and the actions row stacks (Save full width, Reset under it). */ .theme-colors { grid-template-columns: 1fr; } .theme-actions { flex-direction: column; align-items: stretch; } #theme-save { width: 100%; } .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 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; } main { padding-bottom: env(safe-area-inset-bottom, 0); } /* Sync + History refresh: full-width LABELED pills on mobile (the icon-only squeeze is gone — a symbol-only control is too small a target to discover on a phone). The RAG page-head row wraps so the Sync pill drops below the "Knowledge base" title at full width; the History page-head already wraps the pill below its title block. The Sync label's min(16rem, 40vw) cap lifts so the live-file text ellipsizes against the FULL width (min-width: 0 lets the nowrap flex item shrink, which is what engages the ellipsis); the refresh glyph joins its visible label. The aria-labels keep the accessible names. */ .page-head-row { flex-wrap: wrap; } .sync-btn { width: 100%; } .sync-label { max-width: none; min-width: 0; } .history-refresh { width: 100%; } .history-refresh svg { display: block; } .sync-result { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } } /* 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 .is-open rule (0,2,0) would otherwise out-specify a bare .app-nav (0,1,0) and the OPEN transition would still animate. */ @media (prefers-reduced-motion: reduce) { .app-nav, .app-nav.is-open { transition: none; } }