Files
brain-of-reese/frontend/assets/styles.css
T

1704 lines
54 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
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 <html> (the canvas). <body> 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: <html> is the root stacking context, so their
z-index:-1 pseudo-elements paint ABOVE the var(--bg) canvas and
BELOW the transparent, non-stacking <body>'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). */
.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;
}
/* Mono wordmark with letter-spacing — the "technical" touch (phase 08). */
.brand-text {
font-family: var(--mono);
font-size: 0.95rem;
letter-spacing: 0.08em;
}
.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; }
.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 <details>/<summary> — 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; }
.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; }
/* ---------- 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; }
/* ---------- Document viewer (phase 10) ---------- */
/* Phase 12: the same fixed-height bar as .app-header (--header-h, 64px /
58px mobile) — the header must never change size between chat,
sources, and the document viewer (owner report 2026-08-22). */
.doc-header {
position: sticky;
top: 0;
z-index: 20;
background: var(--surface);
height: var(--header-h);
/* Phase 12: same guard as .app-header — the bar never shrinks. */
flex-shrink: 0;
}
.doc-header-inner {
height: 100%;
display: flex;
align-items: center;
gap: 0.9rem;
}
/* Back link: pill with an SVG arrow + "Sources" (>=44px touch target). */
.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; }
.doc-title-block { min-width: 0; }
/* Phase 19: the shared header controls reach the viewer bar (New chat
+ Sign in / Sign out) — margin-left:auto pushes them to the right;
the title block keeps clipping (min-width: 0 above) so the two pills
fit while the bar still measures exactly --header-h. The reused
.new-chat-btn / .auth-link classes already carry the ≤640px icon-only
rules, so at 360px the bar is back pill + clipping title + two icon
pills (no overflow — test_responsive_polish pins scrollWidth). */
.doc-header-actions {
margin-left: auto;
display: flex;
gap: 0.5rem;
align-items: center;
}
#doc-title {
margin: 0;
font-size: 1.3rem;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Meta row: source badge · format badge · mono path · indexed · chunks.
Phase 12: it may clip, but it must NEVER wrap — a wrapped meta row
would grow the header past the shared --header-h bar. */
.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; }
/* 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 (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. */
.header-inner { gap: 0.5rem; }
.brand { min-width: 0; }
.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.4rem 0.4rem; font-size: 0.84rem; }
.app-nav { gap: 0.15rem; }
.new-chat-btn { padding: 0.4rem 0.55rem; }
.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.55rem; }
.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.55rem; }
.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.55rem; }
/* 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; }
.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-header-inner { flex-wrap: nowrap; gap: 0.5rem; } /* phase 12: fixed-height bar — no wrap, no extra padding */
#doc-title { font-size: 1.1rem; }
.doc-path { max-width: 16rem; }
.doc-md { padding: 1.1rem 1rem; }
.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; }
.footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; }
main { padding-bottom: env(safe-area-inset-bottom, 0); }
}