/* ========================================================================== 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 */ --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 */ --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) */ --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; } * { box-sizing: border-box; } html, body { 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; } /* ---------- Animated background (pure CSS, zero JS — phase 08; reworked phase 25: no movement, only fading light) ---------- Owner direction (2026-08-25, verbatim): "It should be smooth, fluxuating, dimming and brightening, but not moving. Different bright spots should slowly fade in and out." - NO movement anywhere in the background: no grid drift, no transform/scale, no background-position animation. The 44px/60s grid drift (0.73px/s, diagonally down-right) rasterizes sub-pixel by sub-pixel and reads as a once-per-second jitter; the 14s whole-layer opacity+scale pulse reads as a uniform blink. Both are gone. - Three independent soft glow spots, each fading in and out on its own SLOW opacity-only cycle — 26s / 34s / 42s, ease-in-out, with negative delays (-12s, -23s) so the cycles run out of phase (LCM 4641s: the composite pattern effectively never repeats within a viewing session). The total light fluxuates smoothly and irregularly. - html::before / html::after join body::before / body::after as background layers: is the root stacking context, so their z-index:-1 pseudo-elements paint ABOVE the var(--bg) canvas and BELOW the transparent, non-stacking 's content — the no-occlusion contract (html owns the canvas, body stays transparent) is unchanged. - No filter (phase-08 no-blur perf anchor), no JS, no new assets; opacity-only keyframes stay compositor-friendly. - prefers-reduced-motion: reduce stills all four layers. */ /* Static grid texture: 44px cells, 1px lines at 60% --line alpha, 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. */ body::before { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background-image: linear-gradient(to right, rgb(74 38 38 / 0.6) 1px, transparent 1px), linear-gradient(to bottom, rgb(74 38 38 / 0.6) 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%); } /* Glow spot A — the phase-08 indigo (top-left): one soft radial spot fading in and out on its own 26s opacity-only cycle. */ body::after { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background-image: radial-gradient(circle 56rem at 12% 8%, rgb(244 63 94 / 0.10), transparent 62%); animation: bg-glow-a 26s ease-in-out infinite; } /* Glow spot B — the phase-08 cyan (bottom-right): 34s cycle, -12s delay (out of phase with spot A). */ html::before { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background-image: radial-gradient(circle 60rem at 88% 92%, rgb(251 146 60 / 0.08), transparent 62%); animation: bg-glow-b 34s ease-in-out -12s infinite; } /* Glow spot C — a third indigo (bottom-left): 42s cycle, -23s delay (out of phase with spots A and B). */ html::after { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background-image: radial-gradient(circle 52rem at 14% 86%, rgb(239 68 68 / 0.08), transparent 62%); animation: bg-glow-c 42s ease-in-out -23s infinite; } /* Opacity-only fades — nothing but opacity may appear in any bg-* keyframe (the no-movement contract, phase 25). */ @keyframes bg-glow-a { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } } @keyframes bg-glow-b { 0%, 100% { opacity: 0.20; } 50% { opacity: 1; } } @keyframes bg-glow-c { 0%, 100% { opacity: 0.15; } 50% { opacity: 1; } } .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: rgb(244 63 94 / 0.45); color: var(--ink); } /* ---------- Header ---------- */ .app-header { height: var(--header-h); background: var(--surface); position: sticky; top: 0; z-index: 20; /* Phase 12: body is a definite-height flex column; 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→cyan gradient hairline under the sticky header (phase 08; shared by the app header and the document-viewer header, phase 10). 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, rgb(244 63 94 / 0.55), rgb(251 146 60 / 0.30) 45%, rgb(251 191 36 / 0.05) 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; } .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: #f55a72; 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 50 (owner-locked 2026-08-29, TODO.md L5): the "Save" pill — the EXACT visual family of .new-chat-btn (same declarations, so the two 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). */ .save-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; } .save-chat-btn:hover { background: #f55a72; color: var(--bg); } /* The save mark is hidden on desktop (the label carries the pill); it is the whole control below 640px (mirrored in the ≤640 block below). */ .save-chat-btn svg { width: 16px; height: 16px; display: none; } /* Phase 51 (owner-locked 2026-08-29, TODO.md L6): the "Share" pill — the EXACT visual family of .save-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 Save 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: #f55a72; 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; } /* 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. */ .chat-shell { max-width: 46rem; margin-inline: auto; display: flex; flex-direction: column; gap: 1rem; flex: 1; } /* 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: #1a0f0f; color: #e6d0d0; 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 46rem column — 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: rgb(10 14 23 / 0.16); } .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; } /* Agent tool-call lines (phase 37): one visible "calling tool" row per `tool` SSE frame — in the same wrap as the Thinking block, above the answer, below the Thinking summary. Deliberately distinct from the scratchpad: accent palette (--accent-ink) vs the brand-ink summary, own icon, own accent left border. Contrast: --accent-ink on the row's --surface ≈10.4:1 (11.6:1 on the page bg), and --ink on --brand-soft in the path `code` ≈11.5:1 — all comfortably AA in the (single dark) theme. Inline rows only: appending lines never shifts the 46rem chat column (no new container), and the rows are not interactive — no focus targets. */ .tool-calls { display: flex; flex-direction: column; gap: 0.25rem; } .tool-call { display: flex; align-items: baseline; gap: 0.45rem; background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--accent-line); border-radius: var(--radius-sm); padding: 0.3rem 0.75rem; color: var(--accent-ink); font-size: 0.8rem; line-height: 1.4; overflow-wrap: anywhere; } .tool-call code { font-family: var(--mono); font-size: 0.95em; background: var(--brand-soft); color: var(--ink); padding: 0.05em 0.35em; border-radius: 5px; overflow-wrap: anywhere; } .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: #2a345f; 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; } /* As flex items these chips must be allowed to shrink (min-width:auto would let a long title-derived chip exceed the column on phones — phase 07 overflow fix); the label text then wraps inside the pill. */ .maybe-try .suggestion-chip { min-width: 0; max-width: 100%; } /* ---------- 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 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; } /* 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: #1a0f0f; 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: #7d88f5; } .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 rgb(110 231 168 / 0.35); 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: #1a0f0f; 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. Same width discipline as the chat column — a centered, capped column on the 72rem frame; the form and the note list span its FULL width (no skinny lists). Every interactive target is >=44px; text pairs reuse the Phase-08 AA palette (dark ink on brand 5.2:1, brand-ink/brand-soft 6.9:1, ok 10.6:1, err 9.1:1, ink-soft >=6.9:1). No filter: blur, no CDN, system font stack. */ .tuning-shell { max-width: 46rem; margin-inline: auto; 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: #7d88f5; } #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: #1a0f0f; 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 rgb(110 231 168 / 0.35); 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 */ .typing { display: inline-flex; 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; } } @media (prefers-reduced-motion: reduce) { .typing span { animation: none; opacity: 0.7; } /* Phase 17 thinking block: the chevron stills (no rotation motion). */ details.thinking summary::before { transition: 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; } .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; } .suggestion-chip:hover { background: #2a345f; } .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 46rem column) 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. */ .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: #7d88f5; } .send-btn:disabled { background: #a5b4fc; cursor: not-allowed; } /* Phase 48 (2026-08-29, TODO.md L3): the in-flight Stop treatment — one button, two roles. Rose-700 #be123c (the brand rose #f43f5e darkened) with a #fff label = 6.3:1 (WCAG AA); the hover step #9f1239 holds 8.0:1. 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: #be123c; color: #fff; } .send-btn.is-stop:hover { background: #9f1239; } /* Busy spinner: dark arc (--bg) on the #a5b4fc busy button = 9.7:1. */ .spinner { width: 16px; height: 16px; border: 2.5px solid rgb(10 14 23 / 0.30); 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; } } /* The background layers are the only other motion on the page: under reduced motion they go static (grid + glows remain, just still) — all four layers (phase 25: the html::before / html::after spots join). */ @media (prefers-reduced-motion: reduce) { body::before, body::after, html::before, html::after { animation: none; } } /* ---------- 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: #f55a72; color: var(--bg); } .stale-regenerate:disabled { opacity: 0.6; cursor: wait; } .stale-regenerate svg { width: 16px; height: 16px; display: block; } /* ---------- 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: #1a0f0f; 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: #7d88f5; } .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; } .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; } } .sync-label { display: inline; } /* 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: rgba(10, 14, 23, 0.82); 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: rgb(239 68 68 / 0.15); } @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: #7d88f5; } .table-wrap { 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; } /* ---------- 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; } #git-source-url { 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 { color: var(--ink-soft); } #git-source-url: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: #7d88f5; } #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: rgb(239 68 68 / 0.12); } /* 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; } /* 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; } /* Per-row delete failure (role=alert): the err pair, inline after the (re-enabled) button. */ .git-source-row-error { margin-left: 0.6rem; background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.3rem 0.6rem; font-size: 0.8rem; font-weight: 600; } /* 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 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: rgb(239 68 68 / 0.18); } .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; } /* ---------- Shared page (phase 51, task 03) ---------- /shared/: the anonymous read-only conversation (owner-locked 2026-08-29, TODO.md L6). The shell maps to the PLAN §7 centered 46rem chat column — the conversation reads exactly like the chat page (the .msg/.bubble/.thinking/.tool-calls/.msg-meta rules apply unchanged) with NO composer, so the column contract holds for a guest. Zero interactive controls (owner-locked): the chips are 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: 46rem; /* the PLAN §7 centered chat column */ 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; 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: #2a345f; } .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 · indexed · chunks. 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); } .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, ≤46rem reading column (PLAN §7.1). */ .doc-md { width: 100%; max-width: 46rem; 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: #1a0f0f; color: #e6d0d0; 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 ≤46rem centered reading column — 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: 46rem; 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); /* #fca5a5 on --surface ≈9.0:1 */ } .doc-summary-text { margin: 0; color: var(--ink); /* on --surface ≈14.5:1 */ } /* 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: #7d88f5; } /* 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 ≤46rem reading column, 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: rgba(10, 14, 23, 0.82); 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; box-shadow: 0 24px 80px rgb(0 0 0 / 0.55); } /* 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 · 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 ≤46rem centered reading column inside; .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; } .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; } } /* ---------- 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, .save-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; } /* 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; } .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; } /* 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 50: the Save pill squeezes with New chat (same family, same rules — the chat-shell overrides below keep both labels visible). */ .save-chat-btn { padding: 0.4rem 0.3rem; } .save-chat-label { display: none; } .save-chat-btn svg { display: block; } /* Phase 51: the Share pill squeezes with Save (same family, same rules — the chat-shell overrides below keep both labels 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 .save-chat-label { display: inline; } .chat-shell .save-chat-btn svg { display: none; } .chat-shell .share-chat-label { display: inline; } .chat-shell .share-chat-btn svg { display: none; } /* 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%; } /* 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, #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 51: the shared page squeezes like the chat column — the title and the note step down (the empty-state-title family); the shell keeps its 46rem column (it is already the narrowest box on the page) 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 button goes icon-only on mobile; the label hides, aria-label keeps the accessible name. The spinning icon is the visible running state on a touch screen. */ .sync-btn { padding: 0.4rem 0.3rem; } .sync-label { display: none; } .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; } }