/* ========================================================================== Brain of Reese — design system (no CDN; system fonts only) Dark tech 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: #0a0e17; /* page: ink on bg 16.2:1 */ --surface: #121a2e; /* ink on surface 14.5:1 */ --ink: #e8ebf4; --ink-soft: #9aa4bd; /* 6.9:1 on --surface */ --line: #26304a; /* decorative 1px borders */ --brand: #6d78f2; /* text on brand is DARK ink (--bg): 5.2:1 — never white on brand (3.7:1, fails) */ --brand-soft: #232b52; --brand-ink: #a5b4fc; /* 8.7:1 on --surface, 6.9: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: #2d1318; --err-ink: #fca5a5; /* 9.1: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(38 48 74 / 0.6) 1px, transparent 1px), linear-gradient(to bottom, rgb(38 48 74 / 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(109 120 242 / 0.14), 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(34 211 238 / 0.10), 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(109 120 242 / 0.09), 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(109 120 242 / 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(109 120 242 / 0.55), rgb(34 211 238 / 0.30) 45%, rgb(34 211 238 / 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); } /* "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: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-weight: 600; font-size: 0.95rem; white-space: nowrap; cursor: pointer; } .new-chat-btn:hover { background: var(--brand-soft); color: var(--brand-ink); } /* 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 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; } /* Phase 32: the admin-only "Sync sources" pill (Sources header) — the same ghost pill as New chat / the auth links, so the bar keeps one visual language. ink-soft on surface ≈6.9:1 (WCAG AA); hover pair brand-ink/brand-soft ≈6.9:1. The refresh icon is always visible (it doubles as the running-state spinner); icon-only below 640px like the other pills (aria-label keeps the accessible name). ≥44px touch target at every width; :focus-visible via the global rule. */ .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; } .sync-btn:hover { background: var(--brand-soft); color: var(--brand-ink); } .sync-btn:disabled { opacity: 0.6; cursor: wait; } /* Phase 34: the failed-sync look — the button now lives on EVERY page (the non-Sources pages have no error banner, so the button itself is the visible failure state; the sanitized error text rides in title / aria-label, set by the header module). Phase-08 error pair: --err-ink on --err-bg ≈9.1:1, --err-line border (the amber --accent-line is deflection-only — never on errors). */ .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; } /* Running state: the refresh icon spins (reuses the shared spin keyframes) — the visible half of "Syncing…" while the 2 s poll waits. */ .sync-btn .sync-icon.is-spinning { animation: spin 1s linear infinite; } @media (prefers-reduced-motion: reduce) { .sync-btn .sync-icon.is-spinning { animation: none; } } /* The aria-live last-result announcer ("2 added · 1 pruned") — soft ink on the header surface (≈6.9:1), small mono to match the stat cards. */ .sync-result { color: var(--ink-soft); font-family: var(--mono); font-size: 0.8rem; white-space: nowrap; } /* "Tuning" toggle (phase 15): ghost pill like New chat + a mono count badge (brand-ink on brand-soft ≈6.9:1). The label is visually-hidden (not removed) below 640px so the accessible name keeps the word. ≥44px touch target at every width. */ .steering-toggle { 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; } .steering-toggle:hover, .steering-toggle[aria-expanded="true"] { background: var(--brand-soft); color: var(--brand-ink); } .steering-toggle svg { width: 16px; height: 16px; display: block; } .steering-count { font-family: var(--mono); font-size: 0.78rem; font-weight: 700; min-width: 1.35rem; text-align: center; padding: 0.05rem 0.4rem; border-radius: 999px; background: var(--brand-soft); color: var(--brand-ink); } .steering-toggle[aria-expanded="true"] .steering-count { background: var(--brand); color: var(--bg); /* dark ink on brand: 5.2:1 */ } /* ---------- Main frame ---------- */ .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; } .messages { 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: #0d1120; color: #e6e9f2; 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; } .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: hidden; /* no user scroll back (owner choice 2026-08-24): the window is a live tail only — the phase-17 JS bottom-pin (scrollTop = scrollHeight per chunk) is the sole scroller */ } /* 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); } /* 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: #0d1120; 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: #0d1120; 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: #0d1120; 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 ---------- */ .composer { display: flex; align-items: flex-end; gap: 0.6rem; 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; } /* 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; } /* ---------- 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: #0d1120; 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-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; } .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, local paths may contain anything, so both inputs are mono) + the brand button; wraps to a column at narrow widths (the <=640px block below). Phase 38: the "Local directory" form (#local-source-form) reuses the git form's rules VERBATIM — one form language for both kinds. */ #git-source-form, #local-source-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, #local-source-form:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); } #git-source-form > label, #local-source-form > label { color: var(--ink); font-weight: 600; white-space: nowrap; } #git-source-url, #local-source-path { 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, #local-source-path::placeholder { color: var(--ink-soft); } #git-source-url:focus-visible, #local-source-path:focus-visible { outline-offset: 0; border-color: var(--brand); } #git-source-add, #local-source-add { 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), #local-source-add:hover:not(:disabled) { background: #7d88f5; } #git-source-add:disabled, #local-source-add:disabled { opacity: 0.6; cursor: wait; } /* 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); } /* ---------- 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: #0d1120; color: #e6e9f2; 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); /* #a5b4fc on --surface ≈8.7: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 + Sync sources + New chat + Sign out) outgrows a 768px bar in the desktop styles. Squeeze the pills/gaps moderately — the 44px touch floor is held by min-height and the 64px height by --header-h — and let the brand wordmark (base ellipsis) absorb any remainder. The ≤640 block below stays tighter and wins at phone widths. */ @media (max-width: 900px) { .header-inner { gap: 0.6rem; } /* Phase 35: the admin-only "Git sources" link joins the nav — a fourth text pill on the admin bar — so the tablet squeeze tightens once more to hold 768px in the admin state (brand already the designated clip target, pills squeeze next). */ .nav-link { padding: 0.4rem 0.5rem; font-size: 0.85rem; } .app-nav { gap: 0.15rem; } .new-chat-btn, .auth-link, .sync-btn, .steering-toggle { 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; } .nav-link { padding: 0.3rem 0.25rem; font-size: 0.72rem; } .app-nav { gap: 0.05rem; } .new-chat-btn { padding: 0.4rem 0.3rem; } .new-chat-label { display: none; } .new-chat-btn svg { display: block; } /* 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 32: the sync pill goes icon-only like the other pills (the 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; } /* The last-result counts stay ANNOUNCED (aria-live is untouched) but go visually hidden — the 58px bar has no room for the text; the icon carries the visible state. Same clip recipe as .steering-label. */ .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; } .steering-toggle { padding: 0.4rem 0.3rem; } /* Visually hidden, NOT display:none — the accessible name keeps the word "Tuning" next to the count badge. */ .steering-label { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } .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; } /* 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 38: + the local directory form): the add forms stack like the other cards — label, full-width mono input, full-width button; the table wrapper's horizontal scroll already covers long URLs/paths. */ #git-source-form, #local-source-form { flex-direction: column; align-items: stretch; } #git-source-form > label, #local-source-form > label { white-space: normal; } #git-source-url, #local-source-path { min-width: 0; } #git-source-add, #local-source-add { width: 100%; } .footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; } main { padding-bottom: env(safe-area-inset-bottom, 0); } }