— the in-container span label has no
chevron to still). */
summary.tool-calls-summary::before { transition: none; }
/* Phase 161 reasoning bubble: the family chevron, the family still. */
summary.reasoning-summary::before { transition: none; }
}
/* Phase 117 (owner request 2026-09-15, live-mockup-confirmed — the
phase-109 D16 dot cue replaced by a BRAIN-WAVE; the owner picked the
ECG over the first equalizer mock): a compact trace (P bump, QRS
spike, T bump — 49px wide, 8px flat tails; the middle is never
rescaled) in index.html as a static SVG. .bw-ghost is the
always-visible dim trace (the shape reads even when the sweep is
between peaks); .bw-pulse is the same path in brand with a
42/140 stroke-dash segment that TRAVELS the path — the classic
heart-monitor draw (the bwdraw keyframe below). Still the phase-109
D16 contract: visible for the ENTIRE active turn (send → terminal
frame), its hidden attribute owned solely by app.js's setUiState
(shown iff inFlight — hidden by construction in every terminal
state, PLAN §7.4). DECORATIVE (aria-hidden in index.html):
#send-status carries the state's meaning, so contrast is N/A here
(§7.2 "text + color, never color alone" — the state TEXT stays in
#send-status). The container is 44px = .send-btn min-height (house
cross-reference — keep in lockstep), the trace centered against the
button. The NEW bwdraw family is deliberate: the phase-109 "reuse
the typing animation" pin is superseded by the owner's redesign.
The loader sits LEFT of the button in the markup (index.html) so
its appearance/disappearance never shifts the button. */
.turn-loader {
display: inline-flex;
align-items: center; /* center the trace against the button */
width: auto;
height: 44px; /* = .send-btn min-height (44px) — keep in lockstep */
background: none;
border-radius: 0;
opacity: 1;
align-self: flex-end; /* the composer row is flex-end aligned */
animation: none; /* the sweep carries the motion, not the container */
}
.turn-loader svg {
width: 49px;
height: 32px;
overflow: visible; /* the QRS spike (y=3) may exceed the box top */
}
.turn-loader .bw-ghost {
fill: none;
stroke: var(--ink-soft);
stroke-opacity: 0.3; /* the dim always-visible trace */
stroke-width: 2;
}
.turn-loader .bw-pulse {
fill: none;
stroke: var(--brand);
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
/* The sweep: a 42/140 bright segment on the pathLength-normalized
path — the dash cycle (42 + 98) equals the normalized length, so
the bwdraw loop is seamless (no visible restart point). */
stroke-dasharray: 42 98;
animation: bwdraw 0.9s linear infinite;
}
@keyframes bwdraw {
from { stroke-dashoffset: 140; }
to { stroke-dashoffset: 0; }
}
/* Phase 117: reduced motion — the sweep is STILLED (the full trace
shows static, no travel — §7.2 house law), never HIDDEN: visibility
is owned by the JS hidden attribute in setUiState (D16), not by
CSS. This override MUST sit AFTER the main rules above: equal
specificity, and the later rule wins the cascade — a media block
placed before the main rule would lose the cascade and the sweep
would keep traveling under reduced motion (the phase-109 cascade
pitfall, carried over). */
@media (prefers-reduced-motion: reduce) {
.turn-loader .bw-pulse { animation: none; stroke-dasharray: none; }
}
/* ---------- Empty state & suggestions ---------- */
.empty-state {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2.5rem 1.75rem;
text-align: center;
margin-top: 1rem;
}
.empty-state-glyph { color: var(--brand-ink); width: 44px; height: 44px; margin-inline: auto; }
.empty-state-glyph svg { width: 44px; height: 44px; display: block; }
.empty-state-title { margin: 0.8rem 0 0.4rem; font-size: 1.5rem; color: var(--ink); }
.empty-state-sub { margin: 0 auto 1.25rem; max-width: 34rem; color: var(--ink-soft); }
.empty-state-sub code { font-family: var(--mono); font-size: 0.85em; background: var(--brand-soft); padding: 0.1em 0.35em; border-radius: 5px; }
.suggestions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
list-style: none;
margin: 0;
padding: 0;
}
/* The shared chip pill — both chip rows (the onboarding row in the
empty state and the "Maybe try" row under a deflected bubble) reuse
it. Phase 104 (owner 2026-09-12, A1): a chip is ONE line at every
viewport width — `white-space: nowrap` stops a 400-char question
from wrapping the pill into a multi-line "chonk"; `overflow: hidden`
zeroes the flex item's automatic minimum size (min-width:auto would
pin the item to the full text width), so `max-width: 100%` binds and
`text-overflow: ellipsis` clips the text at the row edge — in the
desktop wrap row 100% is the chat column, in the ≤640px row
(nowrap + overflow-x: auto) 100% is the VISIBLE width and the row
scrolls. That clipping is the phase-07 overflow fix the old
"Maybe try" chip override used to carry (min-width:0 + max-width:
100%) — now fully subsumed here and deleted, provenance folded into
this comment so the history lives with the contract. The full text
stays one hover away (the title tooltip, phase 104 task 02). */
.suggestion-chip {
font: inherit;
font-size: 0.92rem;
font-weight: 600;
color: var(--brand-ink);
background: var(--brand-soft);
border: 1px solid var(--line);
border-radius: 999px;
padding: 0.55rem 1rem;
min-height: 44px;
cursor: pointer;
transition: background 0.15s ease, transform 0.05s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
min-width: 0;
}
.suggestion-chip:hover { background: var(--brand-soft); }
.suggestion-chip:active { transform: scale(0.98); }
/* ---------- Composer ---------- */
/* Phase 52 (2026-08-30, TODO.md L3): the composer is PINNED to the
viewport bottom. The page scrolls at the document level and
`.chat-shell` (the centered reading column — 72rem, the
.container width at every viewport) is the composer's sticky
containing block, so the box sticks to the bottom edge of the
viewport at every scroll position and settles back into its normal
flow position (above the footer) once the document bottom is
reached. Before this, a long conversation put the composer below the
fold while phase 42 forbids auto-scrolling a streaming reply — the
Stop control could literally not be reached ("it runs away from the
user as they try to click stop").
The pin is TWO rules working together — sticky alone is not enough
(owner revision 2026-08-30: "the chat message-input textarea should be
at the bottom of the screen"). `position: sticky` can only pull the
box UP to the viewport's bottom edge while the document overflows; it
never pushes it DOWN, so on an empty/short chat the box still sat
mid-screen. `.messages { flex: 1 1 auto }` supplies the missing half:
it puts the box's resting position at the bottom of the full-height
column, and sticky takes over the moment the conversation overflows.
The offset is the notch-aware safe-area inset (`env()` resolves to 0,
or falls back to 0 where unsupported, so the box sits flush with the
viewport bottom; ≤640px keeps its own
`main { padding-bottom: env(safe-area-inset-bottom) }`).
CSS-only by design: no DOM change, no JS, and NO new scroll call
site (the phase-42 never-auto-scroll contract stays intact —
`scrollReveal` is still the one page scroll in app.js).
The solid `--surface` background, border, radius and shadow are kept
so messages scrolling behind the pinned box never show through it,
and no z-index is added: DOM order already paints the composer over
`.messages`, it never overlaps the sticky header (z 20) and stays
under the z-1000 document modal.
Phase 65 (2026-09-01, `TODO.md` L3, owner-locked A1): the pin now
belongs to the .chat-bottom wrapper (rule below) — the row + this
form are ONE sticky unit; this composer's own sticky declaration is
kept, redundant inside the wrapper but pinned as a computed style by
tests/e2e/test_pinned_composer.py. */
/* Phase 65 (owner-locked A1, 2026-09-01, `TODO.md` L3): the pinned
bottom cluster — the .chat-actions row + the #composer form as ONE
sticky unit (A1): the pills are literally always at the bottom of
the screen, even while the reader is scrolled up through a long
conversation (the rejected in-flow alternative would scroll away).
The sticky range is the .chat-shell containing block (the wrapper's
parent) — the same range the composer used to have alone — and the
.messages flex-grow still rests the WHOLE unit at the screen bottom
on short chats (the wrapper is the last shell child, so the grow
pushes it down). The shell's 1rem gap applies only ABOVE the unit,
from .messages — the 0.5rem gap here is the row↔composer spacing
INSIDE it. No z-index: the phase-12 sticky header (z 20) and the
phase-46 dropdown always stay on top, and at the document bottom the
unit settles back into flow above the footer (the composer's opaque
surface keeps messages hidden where the box overlaps them). The
composer's own sticky rule below stays — redundant here (its
containing block is the wrapper, so it cannot shift within the
unit), pinned by tests/e2e/test_pinned_composer.py. */
.chat-bottom {
display: flex;
flex-direction: column;
gap: 0.5rem;
position: sticky;
bottom: env(safe-area-inset-bottom, 0);
}
/* Phase 88: pre-boot, the composer cluster is NOT sticky. The sticky
promotion (a compositor layer) is deferred out of the first layout
commit — on real Android, the cluster's sticky layer born at first
commit was eating the hamburger's click (owner bug report
2026-09-08; the client-side switch into chat — same layer, born on
a settled page — always worked). app.js adds .chat-booted two
frames after boot settles; no-JS keeps the cluster static, which in
the only no-JS state (empty, 100dvh document) sits exactly where
the pin would. */
#view-chat:not(.chat-booted) .chat-bottom,
#view-chat:not(.chat-booted) .composer {
position: static;
}
/* Phase 104 (owner 2026-09-12, A4): the question-length counter —
right-aligned above the composer, a child of the .chat-bottom sticky
unit (between the actions row and the form), hidden until 80% of the
4,000-char cap (app.js updateCharCount). It sits on the APP
background behind the transparent .chat-bottom unit, so both pairings
are verified against --bg: --ink-soft on --bg = 8.6:1 (≥4.5:1, WCAG
AA) and the .is-max state's --err-ink on --bg = 10.4:1 (AA). The
.is-max state ALSO changes the copy ("— character limit") — text +
color, never color alone (B3). */
.char-count { margin: 0; text-align: right; font-size: 0.75rem; line-height: 1.2; color: var(--ink-soft); }
.char-count.is-max { color: var(--err-ink); }
/* Phase 123 (task 02, TODO L6): the ATTACH PREVIEW STRIP — the
selected image ABOVE the input row (thumbnail ≤48px + the filename
+ the remove button): a surface card in the .chat-bottom stack, between
the char-count line and the composer. The name is ellipsized
(AA-safe --ink on --surface = 13.8:1) — a long filename never widens
the strip; the thumbnail is a fixed 48px cover box (the preview
crops, the bubble shows the whole image); the remove button keeps the
44px touch
target (PLAN §7.1) with a destructive hover (--err-ink on --err-bg =
9.3:1 — text + color, never color alone, B3). Hidden by default
(the global [hidden] rule) — revealed only while a file is attached,
cleared with the send so the strip never lingers into a turn. */
.attach-preview {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0 0 0.45rem;
padding: 0.35rem 0.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
}
.attach-preview-img {
flex: none;
width: 48px;
height: 48px;
object-fit: cover;
border: 1px solid var(--line);
border-radius: 4px;
background: var(--bg);
}
.attach-preview-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.85rem;
color: var(--ink);
}
.attach-preview-remove {
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
width: 44px;
min-height: 44px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
cursor: pointer;
padding: 0;
}
.attach-preview-remove svg { width: 18px; height: 18px; }
.attach-preview-remove:hover { background: var(--err-bg); color: var(--err-ink); }
/* Phase 132 (task 02, TODO L8): the strip's UPLOADING look — the
visual half of the owner-confirmed double-spam block (task 01's JS
state proves the image is moving while POST /api/chat-images is in
flight; before this the strip looked frozen and the user spammed
Send). The thumbnail DIMS to 0.5 (it is decorative — no contrast
duty — and 0.5 is the house dim value the disabled remove button
below); the filename HIDES with visibility (the flex: 1 slot keeps
its measured width — the status takes the row's right hand WITHOUT
reflowing the pinned 44px remove button); the house loading arc
(the status part's svg — task 01's markup, currentColor = the
status's --ink-soft) SPINS at the sync spinner's pace (the house
`spin` keyframes at 1s linear — .sync-icon.is-spinning's in-flight
pace; the .spinner's 0.8s is the busy-turn arc's). The status text
is --ink-soft on the strip's --surface — the :root-pinned pair,
8.2:1 computed against WCAG 2.1 (AA ≥ 4.5:1), at the strip's
small text size (the name's 0.85rem). The remove button (task 01
disables it mid-flight) dims with its disabled attribute — the
attribute is the state, the 0.5 is its visual half; the Send rides
the same house :disabled language (.send-btn:disabled's
--brand-busy + not-allowed below — which reads as disabled already,
so NO new color or weight was added, B4). prefers-reduced-motion stills the arc (the house block): the
"Uploading image…" TEXT + the dimmed thumbnail carry the state
without motion (B5 — never motion alone). Every value is a theme
variable or a neutral number — the block ships no raw color. */
.attach-preview.is-uploading .attach-preview-img {
opacity: 0.5;
}
.attach-preview.is-uploading .attach-preview-name {
visibility: hidden; /* keep the flex: 1 slot; the status takes its right hand */
}
.attach-preview-status {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--ink-soft);
font-size: 0.85rem; /* the strip's small text — the name's size */
min-width: 0; /* the row's clamp: at 320 px the text WRAPS (no clip) */
}
.attach-preview-status-icon {
width: 14px;
height: 14px;
flex: 0 0 auto;
animation: spin 1s linear infinite; /* the house spin keyframes (the sync pace) */
}
@media (prefers-reduced-motion: reduce) {
.attach-preview-status-icon { animation: none; }
}
.attach-preview.is-uploading .attach-preview-remove {
opacity: 0.5; /* the disabled attribute's visual half (task 01 sets it) */
}
.composer {
display: flex;
align-items: flex-end;
gap: 0.6rem;
position: sticky;
bottom: env(safe-area-inset-bottom, 0);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 0.6rem;
}
.composer:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); }
.composer textarea {
flex: 1;
font: inherit;
color: var(--ink);
border: 0;
resize: none;
max-height: 12rem;
padding: 0.55rem 0.5rem;
background: transparent;
}
.composer textarea::placeholder { color: var(--ink-soft); }
/* Phase 123 (task 02, TODO L6): the composer's ATTACH CONTROL — the
paperclip glyph button LEFT of the input. Hidden in the markup until
app.js reveals it from the config's images flag (the global [hidden]
rule keeps it out of the flag-off DOM — A5's default-off contract).
A neutral cut of the .send-btn family: the same 44px hit target +
radius + the global :focus-visible ring (PLAN §7.2); surface fill
with a muted hover step (the icon: --ink-soft on --surface = 5.1:1,
hover --brand-ink on --brand-soft = 12.4:1 — both past the 3:1
non-text floor). */
.attach-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
width: 44px;
min-height: 44px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink-soft);
cursor: pointer;
padding: 0;
}
.attach-btn svg { width: 20px; height: 20px; }
.attach-btn:hover { background: var(--brand-soft); border-color: var(--brand-soft); color: var(--brand-ink); }
.send-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
min-width: 84px;
min-height: 44px;
border: 0;
border-radius: var(--radius-sm);
background: var(--brand);
/* Dark ink on brand (5.2:1) — never white on brand (3.7:1, fails). */
color: var(--bg);
font: inherit;
font-weight: 700;
cursor: pointer;
padding-inline: 1rem;
}
.send-btn:hover:not(:disabled) { background: var(--brand-hover); }
.send-btn:disabled { background: var(--brand-busy); cursor: not-allowed; }
/* Phase 48 (2026-08-29, TODO.md L3): the in-flight Stop treatment — one
button, two roles. --brand-stop (the brand darkened 75% toward --bg;
the built-in default mixes to ≈ (187, 50, 73)) with a white label =
5.8:1 (WCAG AA); the hover step (--brand at 55% toward --bg) holds
8.5:1. white is the ONE literal allowed outside :root — the dark bg
ink on the stop fill is only ≈3.4:1 (fails AA). Same radius/height/
hit target as the Send state and the shared :focus-visible ring — the
.is-stop class rides the same .send-btn element, and the later rules
win the hover specificity tie. */
.send-btn.is-stop { background: var(--brand-stop); color: #fff; }
.send-btn.is-stop:hover { background: color-mix(in srgb, var(--brand) 55%, var(--bg)); }
/* Busy spinner: dark arc (--bg) on the --brand-busy button ≈ 11.3:1
(well above the 9:1 floor the phase-48 note pinned for the arc). */
.spinner {
width: 16px; height: 16px;
border: 2.5px solid color-mix(in srgb, var(--bg) 30%, transparent);
border-top-color: var(--bg);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.spinner { animation-duration: 2s; }
}
/* ---------- Banners ---------- */
.kb-banner {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--accent-bg);
color: var(--accent-ink);
border: 1px solid var(--accent-line);
border-radius: var(--radius-sm);
padding: 0.6rem 0.9rem;
font-size: 0.92rem;
font-weight: 600;
}
.kb-banner.is-error { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.kb-banner svg { width: 18px; height: 18px; flex: 0 0 auto; display: block; }
/* Phase 53 (task 05): the stale-saved-chat banner — the .kb-banner
FAMILY (the section carries both classes: same flex row, accent
tokens — accent-ink on accent-bg 9.5:1 — 18px glyph), but the
leading mark is the REDO glyph, distinct from the warning triangle.
It sits directly after #kb-banner in .chat-shell, so when both are
visible the empty-KB banner keeps the top slot and the stale banner
stacks directly below (the flex column's gap spaces them). The text
takes the row; the Regenerate pill right-aligns (margin-left: auto)
and the row wraps only when it must. The pill is the EXACT
brand-pill family of Save/Share: solid --brand, --bg text (5.2:1,
AA), borderless, 999px radius, ≥44px target, hover lightens the
brand fill; the 16px redo glyph is the phase-49 Retry asset. The
≤640px block below makes the pill a full-width row. */
.stale-banner { flex-wrap: wrap; }
.stale-banner > span { flex: 1 1 auto; }
.stale-regenerate {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
margin-left: auto;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 0;
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 700;
font-size: 0.95rem;
white-space: nowrap;
cursor: pointer;
}
.stale-regenerate:hover { background: var(--brand-hover); color: var(--bg); }
.stale-regenerate:disabled { opacity: 0.6; cursor: wait; }
.stale-regenerate svg { width: 16px; height: 16px; display: block; }
/* Phase 111 (task 01): the banner Retry button — the same pill treatment
as .stale-regenerate (brand fill, ≥44px, 999px radius, redo glyph) so
the banner's action reads as the same component family. The kb-banner
flex row + gap already handles inline spacing after #kb-banner-text. */
.banner-retry {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
margin-left: auto;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 0;
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 700;
font-size: 0.95rem;
white-space: nowrap;
cursor: pointer;
}
.banner-retry:hover { background: var(--brand-hover); color: var(--bg); }
.banner-retry:disabled { opacity: 0.6; cursor: wait; }
.banner-retry svg { width: 16px; height: 16px; display: block; }
/* Phase 55 (task 04): the share-success toast (TODO.md L5). Top-right,
just under the sticky header (--header-h + a small offset — the
variable already steps 64px → 58px at ≤640px), z-index 1000 (the
modal overlay contract — above the header's 20 and the skip-link's
100). VISUAL ONLY (A4 owner-locked): the node ships aria-hidden
from app.js — #send-status stays the a11y announcer (no double
screen-reader read). Solid brand fill: --bg text on --brand = 5.2:1
(AA — the .new-chat-btn family); rounded, shadowed, one line of
text that wraps under the small max-width. Hidden by default
(opacity 0 + pointer-events: none — it never intercepts clicks when
idle); .toast.is-visible (toggled by showToast) runs the entry:
slide-down + fade (translateY(-8px) → 0, 200ms). */
.toast {
position: fixed;
top: calc(var(--header-h) + 0.75rem);
right: 1rem;
z-index: 1000;
max-width: min(22rem, calc(100vw - 2rem));
padding: 0.6rem 1rem;
border-radius: var(--radius-sm);
background: var(--brand);
color: var(--bg);
font-size: 0.92rem;
font-weight: 600;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transform: translateY(-8px);
transition: opacity 200ms ease, transform 200ms ease;
}
.toast.is-visible {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
/* No slide: BOTH states rest at transform: none (the .is-visible
rule would otherwise out-specify the bare .toast); the opacity
fade remains (120ms). */
.toast,
.toast.is-visible { transform: none; }
.toast { transition: opacity 120ms ease; }
}
/* ---------- Login page (phase 16) ---------- */
/* Centered card in the standard frame: one admin, one password. */
.login-shell {
display: flex;
justify-content: center;
flex: 1;
}
.login-card {
width: 100%;
max-width: 26rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2rem 2rem 2.25rem;
}
.login-card h1 { margin: 0 0 0.4rem; font-size: 1.6rem; }
.login-sub { margin: 0 0 1.5rem; color: var(--ink-soft); }
#login-form { display: flex; flex-direction: column; gap: 0.75rem; }
#login-password {
font: inherit;
font-size: 1rem;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.55rem 0.75rem;
min-height: 44px;
}
#login-password:focus-visible { border-color: var(--brand); }
/* Brand button: dark ink on brand 5.2:1 (never white on brand). */
.login-submit {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
border: 0;
border-radius: var(--radius-sm);
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 700;
cursor: pointer;
padding-inline: 1rem;
}
.login-submit:hover:not(:disabled) { background: var(--brand-hover); }
.login-submit:disabled { opacity: 0.6; cursor: wait; }
/* Login failure (role=alert): err pair ≈9.1:1. */
.login-error {
margin: 0.9rem 0 0;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.5rem 0.8rem;
font-size: 0.9rem;
font-weight: 600;
}
/* ---------- Sources page ---------- */
.sources-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* Phase 93 task 03 (TODO.md L3): "the header and description of each
page needs a background - the grid makes it hard to read." Every
page's h1 + description sits on a solid surface panel — the house
card language (var(--surface) fill, itself tab-controlled, so a
monochrome theme grays it automatically; the 1px --line border; the
house radius). No new color literal, no blur (the phase-08 perf
anchor). One rule covers the six shell views' .page-head (Global
Tuning, Knowledge base, Git sources, Saved chats, Access tokens,
Theme) + doc-edit.html, and the shared page's head (its h1 + lede
are wrapped in the same class in shared.html). #view-chat carries
no .page-head — its head is the navbar (audited, unchanged). The
History page-head keeps its flex row (scoped below — the padding
lives on the flex container, not the children). */
.page-head {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem 1.25rem;
}
.page-head h1 { margin: 0 0 0.25rem; font-size: 1.7rem; }
.page-head-row {
display: flex;
align-items: center;
gap: 1rem;
}
.page-sub { margin: 0; color: var(--ink-soft); }
.page-sub code { font-family: var(--mono); font-size: 0.85em; background: var(--brand-soft); padding: 0.1em 0.35em; border-radius: 5px; }
/* ---------- Sync button (Sources page) ---------- */
.sync-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.95rem;
white-space: nowrap;
cursor: pointer;
flex-shrink: 0;
}
.sync-btn:hover { background: var(--brand-soft); color: var(--brand-ink); }
.sync-btn:disabled { opacity: 0.6; cursor: wait; }
.sync-btn.is-error {
background: var(--err-bg);
color: var(--err-ink);
border-color: var(--err-line);
}
.sync-btn.is-error:hover { background: var(--err-bg); color: var(--err-ink); }
/* Phase 134 (task 02): the Cancel control — the in-flight sync run's
cooperative stop (visible ONLY while a SYNC run is in flight; the
upload job is not cancellable in this phase — sources.js owns the
show/hide, the same never-stale contract as the main button). The
SECONDARY pill language of the house button family (the
.kb-summary-cancel pair): transparent fill, 1px --line border,
--ink-soft text (5.1:1 on --surface, AA), the global 3px
:focus-visible ring, the >=44px touch floor. :disabled = the cancel
request in flight (one request at a time — never stale); the hover
is guarded on :not(:disabled) so a disabled button under the cursor
never reads as actionable. The [hidden] rule keeps the attribute
authoritative over the display above (the .kb-summary-edit pattern).
No layout regression on the sync-control row: the row already
flexes with a 1rem gap (desktop) and wraps (mobile, where the pill
goes full-width like the main button — the 360px case). */
.sync-cancel-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.95rem;
white-space: nowrap;
cursor: pointer;
flex-shrink: 0;
}
.sync-cancel-btn:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); }
.sync-cancel-btn:disabled { opacity: 0.6; cursor: default; } /* the cancel request is in flight */
.sync-cancel-btn[hidden] { display: none; } /* the hidden attr must beat the display above */
.sync-icon { width: 16px; height: 16px; display: block; flex: 0 0 auto; }
.sync-btn .sync-icon.is-spinning { animation: spin 1s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.sync-btn .sync-icon.is-spinning { animation: none; }
}
/* Phase 64: the live-file label ("Syncing… (n/m)" / "Importing
(n/m)") — a long source/relative/path ellipsizes inside the
pill; the full path lives in the button title + the #sync-progress
box (the aria-live announcer — phase 150: the SOLE sync text
surface). Phase 128 (task 02): the label is a FLEX ROW
of parts — .sync-label-main (the prefix + file/folder: min-width
0, the shrinkable item whose ellipsis engages) + .sync-label-count
(the pinned "(n/m)": flex 0 0 auto, NEVER clipped — the
.sync-progress-text mono language, a position reads as data). Phase 129
(task 04): a THIRD pinned part — .sync-label-chunk (the "· chunk
n/m" embed-batch position of the in-flight file, the same pinned
mono language; hidden in every state but the import phase after
the file's first batch). */
.sync-label {
display: inline-flex;
align-items: baseline;
gap: 0.35rem;
min-width: 0;
max-width: min(16rem, 40vw);
}
.sync-label-main {
min-width: 0; /* the flex item that shrinks — what engages the ellipsis */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sync-label-count {
flex: 0 0 auto; /* NEVER clipped — the (n/m) position is the point */
white-space: nowrap;
font-family: var(--mono); /* the .sync-progress-text mono language (position reads as data) */
}
.sync-label-chunk {
flex: 0 0 auto; /* NEVER clipped — the · chunk n/m position is the point (phase 129) */
white-space: nowrap;
font-family: var(--mono); /* the same pinned-part language as the count (position reads as data) */
}
/* Phase 149 (task 02): the sync detail box — the house's bordered panel
language (the .page-head card: --surface fill, 1px --line border,
the house radius; --line is decorative, no contrast duty, like every
other house border). It sits in the .sources-shell flex column whose
1.25rem gap IS the house spacing above and below — no margin-top
(it would double the gap). min-height ≈ 2 lines (0.8rem mono at the
body 1.55 line-height, + the 0.75rem block padding + the 2px border,
border-box) so even a one-line label reads as a calm panel with
breathing room. The box is hidden-attr-driven (the global [hidden]
!important rule keeps it display:none while its text is empty —
phase 150: it shows while a run is in flight AND stays up after
settle to carry the result line). Mobile: a block in the same
.container rides the 100%-width phone shell — full width with no
override (task 03's E2E asserts it at 360px). */
.sync-progress {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.75rem 1rem;
min-height: 4rem;
}
.sync-progress-text {
display: block;
font-family: var(--mono); /* the house mono language — position reads as data */
font-size: 0.8rem; /* the house small-data size */
line-height: 1.55; /* the house body line-height */
color: var(--ink-soft); /* PINNED identity pair on --surface (8.2:1 on the
built-in dark, AA in both built-ins; the theme gate
keeps any custom theme at >= 4.5:1) */
overflow-wrap: anywhere; /* THE POINT of the box — a long source/relative/path WRAPS
inside the panel; the pre-phase pill ellipsis is retired
for the live detail (never clipped) */
}
/* Phase 149 (task 02): the running button IS the loading spinner (the
owner's ask) — the label parts hide via the .is-running class
(sources.js adds/removes it exactly where the icon's .is-spinning
travels). The pill's padding/border stay, so the control never
collapses: the 16px icon centers in a ~47x44px pill (desktop) or the
full-width 44px bar (mobile — the pre-phase .sync-btn { width: 100% }
rule keeps the big in-flight target, viewport-agnostic rule defined
ONCE here). :hover, :disabled, and :focus-visible are untouched —
they live on the .sync-btn element, and hiding the label span touches
neither. The idle "Sync sources" label (and the mobile ellipsis rules
for it) render exactly as pre-phase when the class is absent. */
.sync-btn.is-running .sync-label { display: none; }
/* Sync failure modal — lazily created by sources.js, appended to .
Error surface: --err-bg with --err-line border, title in --ink,
error text in --err-ink (9.1:1 on --err-bg). Backdrop dims with
--bg at 82%. z-index 1000 (same overlay contract as doc-modal). */
.sync-modal-backdrop {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
background: color-mix(in srgb, var(--bg) 82%, transparent);
visibility: hidden;
opacity: 0;
transition: opacity 120ms ease;
}
.sync-modal-backdrop.is-open {
visibility: visible;
opacity: 1;
}
.sync-modal {
position: relative;
width: 100%;
max-width: 28rem;
background: var(--err-bg);
border: 1px solid var(--err-line);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
padding: 1.1rem 3rem 1.25rem 1.25rem;
}
#sync-modal-title {
margin: 0 0 0.6rem;
font-size: 1.1rem;
color: var(--ink);
}
#sync-modal-error {
margin: 0 0 1rem;
font-family: var(--mono);
font-size: 0.9rem;
color: var(--err-ink);
overflow-wrap: anywhere;
}
.sync-modal-close {
position: absolute;
top: 0.3rem;
right: 0.3rem;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
padding: 0;
border-radius: 999px;
border: 1px solid var(--err-line);
background: transparent;
color: var(--err-ink);
font-size: 1.2rem;
line-height: 1;
cursor: pointer;
}
.sync-modal-close:hover { background: color-mix(in srgb, var(--err-line) 15%, transparent); }
@media (prefers-reduced-motion: reduce) {
.sync-modal-backdrop { transition: none; }
}
.stat-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 0.9rem;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
/* Mono stat values — technical readout (phase 08). */
.stat-value { font-family: var(--mono); font-size: 2rem; font-weight: 800; color: var(--brand-ink); line-height: 1.1; }
.stat-value-sm { font-size: 1.15rem; font-weight: 700; }
.stat-label { color: var(--ink-soft); font-size: 0.88rem; font-weight: 600; }
/* Phase 16: anonymous sign-in gate — the designed replacement for the
catalog (stat cards + table) until the admin signs in. */
.sources-gate {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.4rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2.5rem 1.75rem;
}
.sources-gate-glyph { color: var(--brand-ink); width: 44px; height: 44px; }
.sources-gate-glyph svg { width: 44px; height: 44px; display: block; }
.sources-gate h2 { margin: 0.6rem 0 0.3rem; font-size: 1.4rem; }
.sources-gate-sub { margin: 0 auto; max-width: 30rem; color: var(--ink-soft); }
.sources-gate-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
margin-top: 0.75rem;
padding: 0.5rem 1.4rem;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 */
font-weight: 700;
text-decoration: none;
}
.sources-gate-link:hover { background: var(--brand-hover); }
/* Phase 79 (task 05): the in-app token gate — the gate surface of the
two token-only pages (the shell + the document viewer, one shared
.auth-gate markup each). A body-level FIXED overlay (the
body-level doc-modal precedent): while visible it is the only
CONTENT-level interactive surface — assets/token-gate.js locks the
root (#main, inert) so focus and keyboard traversal stay inside
the gate (WCAG, the inert-pair contract).
Stacking (phase 85, TODO.md L3): z-index 15 — ABOVE all app
content (static) but BELOW the sticky header (20) and its mobile
dropdown (21, inside the header's stacking context): the
unauthenticated visitor keeps a reachable hamburger + menu while
the gate still covers and locks the app content (#main stays
inert — the lock is JS, not z-order — phase 79). The skip-link
(100) and the 1000-family overlays (doc-modal, sync backdrop) are
unaffected — all above 15; the doc-modal can only be opened from
the unlocked app anyway. Solid --bg: the canvas + grid texture
live on (the body stays transparent), so the overlay reads
as the app's own surface — no blur (the phase-08 perf anchor).
The centered card reuses the #sources-gate visual language
(surface card + hairline + glyph + heading + sub + action) in a
tighter column. */
.auth-gate {
position: fixed;
inset: 0;
z-index: 15;
display: flex; /* the card is the only in-flow child — margin: auto centers it */
overflow-y: auto; /* short viewports: the card scrolls fully into view */
background: var(--bg);
}
/* Explicit (the global [hidden] rule already wins — this is the
documented, testable contract for the skeleton). */
.auth-gate[hidden] { display: none; }
.auth-gate-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.4rem;
/* margin:auto centers AND stays scrollable on overflow (the flex
centering pitfall: align/justify center would clip the top of a
taller-than-viewport card). */
margin: auto;
width: min(28rem, calc(100vw - 2rem));
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2.5rem 1.75rem;
}
.auth-gate-glyph { color: var(--brand-ink); width: 44px; height: 44px; }
.auth-gate-glyph svg { width: 44px; height: 44px; display: block; }
.auth-gate h2 { margin: 0.6rem 0 0.3rem; font-size: 1.4rem; }
.auth-gate-sub { margin: 0; max-width: 24rem; color: var(--ink-soft); }
/* The token form: mono input (the token is code — the house mono
language), a visible focus ring (WCAG — the 3px :focus-visible
outline at offset 0 + the brand border), and the house submit
button (the .tune-save language: dark ink on brand, 5.2:1). Both
targets are ≥44px (the touch-target contract). */
.auth-gate form {
display: flex;
flex-direction: column;
gap: 0.6rem;
width: 100%;
margin-top: 0.9rem;
}
.auth-gate input {
font-family: var(--mono);
font-size: 0.95rem;
color: var(--ink); /* 16.7:1 on --bg */
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.65rem 0.8rem;
min-height: 44px;
width: 100%;
}
.auth-gate input::placeholder { color: var(--ink-soft); }
.auth-gate input:focus-visible { outline-offset: 0; border-color: var(--brand); }
.auth-gate-submit {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0.4rem 1.1rem;
border: 0;
border-radius: var(--radius-sm);
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 */
font: inherit;
font-weight: 700;
cursor: pointer;
}
.auth-gate-submit:hover:not(:disabled) { background: var(--brand-hover); }
.auth-gate-submit:disabled { opacity: 0.6; cursor: wait; }
/* The one-line error (role=alert) — the rose/danger family the
house alerts use (--err-ink on --err-bg, 9.3:1). */
.auth-gate-error {
margin: 0.75rem 0 0;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.8rem;
}
/* The secondary "Sign in as admin" link reuses the .sources-gate-link
pill language (its own margin-top: 0.75rem applies) — the guest's
other door: the admin password login (the static ?next=/ is the
no-JS fallback). */
/* Phase 86: the card is the containing block for the
position:absolute .visually-hidden elements it hosts (the table
+ the Actions header span in #view-history /
#view-tokens). Without a positioned ancestor their 1px boxes lay
out against the INITIAL containing block — at the 640px table's
right edge — and leak into the DOCUMENT's scrollable overflow
(the page appeared "the width of the table", TODO.md L4; the
Tokens view carried the identical defect). Zero-offset positioning
changes no layout: the spans stay clipped by their own
clip: rect(0 0 0 0) + 1px box, and cards without such spans (the
RAG .docs-table, the git-sources table) render unchanged.
Phase 156 (task 01, owner-locked A1): the card is ALSO the
vertical scroll region — max-height: min(70vh, 64rem) bounds it
(a 15-row table scrolls, a 3-row one fits without a scrollbar)
and overflow: auto (superseding the phase-86 overflow-x pin —
the tables' min-width still triggers the in-card horizontal
scroll at narrow widths) makes the card the sticky th's
scrolling ancestor; an unbounded card never scrolls vertically,
so a sticky header in it would scroll away with the page. */
.table-wrap {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: auto;
max-height: min(70vh, 64rem);
}
/* Phase 156 (task 01): the ONE sticky-header rule for every
catalogue table inside a .table-wrap card (the RAG .docs-table,
the .git-sources-table, the .history-table, the .tokens-table —
the per-table th rules keep the text styling only). top: 0 pins
the header row to the card top while the card's own vertical
scroll moves; z-index: 1 keeps the pinned th above the scrolled
cells. OPAQUE background: --brand-soft is a solid hex (no alpha),
so rows never show through the pinned header (B3: no new palette
variable; a themed page's #bor-theme override of the token
handles the color — same behavior, B4). */
.table-wrap table thead th {
position: sticky;
top: 0;
z-index: 1;
background: var(--brand-soft);
}
/* Phase 156 (task 02, owner-locked A3): the ellipsis hint on the
catalogue tables' CLIPPED action labels. The class sits on the
LABEL SPAN, never the cell (the docs-table cell padding is
untouched). The five families: the Sources "Hidden" checkbox
text + "Ignore paths" + "Remove" (git-sources.js makeRow), the
RAG folder table's "Edit" (sources.js makeDescCell), the
History "Copy" / "Unshare" / "Create link" / "Delete" (history.js).
max-width: 12rem is GENEROUS — every one of the five labels
("Edit" ~2.4rem … "Ignore paths" ~6.3rem at 0.78–0.82rem)
fits it, so a cell that FITS shows its full text (the clamp
engages only in the CLIPPED case — the audit's ~1,009px
rightmost columns), where the label shows "…" instead of the
card edge's hard cut. Mechanism: the block container's
overflow hidden + text-overflow ellipsis + nowrap is the
house .sync-label shrinkable-item pattern; the SPAN's
min-width: 0 lets the flex item shrink below its text width —
but a flex item shrinks only when its FLEX PARENT is
constrained, so the hosting controls carry the supporting width
rules (the max-width: 100% + min-width: 0 added to each block
below): without them the inline-flex buttons keep their content
size, overflow the squeezed cell, and the card edge hard-cuts
the label again (the pre-phase defect). The FULL text stays
reachable — every one of these controls' aria-label names the
row's identity (the source's URL/path — the phase-89/105 idiom;
the chat title — the phase-66 idiom; the source/folder name —
the phase-97 idiom), so the clip is visual only. B5 idiom
intact: the labels are text (text + color, never color alone). */
.cell-clip {
display: block;
min-width: 0;
max-width: 12rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.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 {
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.docs-table td:nth-child(2) { font-family: var(--mono); font-size: 0.82rem; max-width: 30rem; overflow: hidden; text-overflow: ellipsis; }
.docs-table tbody tr:hover { background: var(--bg); }
.docs-table tbody tr:last-child td { border-bottom: 0; }
/* ---------- KB drill-down tree (phase 97, task 04) ----------
The RAG view lists the catalog the way the agent's `ls` sees it
(the phase-94 concept, one end to end): #kb-crumb (the location
breadcrumb — hidden at the top level), #kb-level (the current
directory's STORED description — the surface-panel language of the
phase-93 page heads), and #folders-table (the ONE folders/sources
table — the .docs-table language; .kb-folders-table only re-styles
the 2nd/4th column cells — phase 106 (task 08, D8) moved
Description from 3rd to 4th, the Updated column taking 3rd). Phase-08
tokens only — NO new hue
(the phase-92 monochrome invariant): brand-ink on surface 9.0:1,
ink 13.8:1, ink-soft 5.1:1 (every pair AA). :focus-visible via the
global 3px outline rule; row targets keep the .docs-table cell
padding (>= 44px effective — the phase-08 touch floor). No CDN,
system fonts. */
.kb-crumb {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.1rem 0.3rem;
margin: 0.9rem 0 0;
font-size: 0.92rem;
}
.kb-crumb-link {
color: var(--brand-ink); /* 10.4:1 on --bg, 9.5:1 on the --brand-soft hover */
text-decoration: none;
padding: 0.25rem 0.35rem;
border-radius: var(--radius-sm);
}
.kb-crumb-link:hover { background: var(--brand-soft); text-decoration: underline; }
.kb-crumb-current {
color: var(--ink); /* 16.7:1 on --bg */
font-weight: 600;
padding: 0.25rem 0.35rem;
}
.kb-crumb-sep { color: var(--ink-soft); }
.kb-level {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem 1.25rem;
margin: 0.9rem 0 0.75rem;
}
.kb-level h2 {
margin: 0 0 0.35rem;
font-family: var(--mono);
font-size: 1rem;
color: var(--brand-ink); /* 9.0:1 on --surface — the path, in the path voice */
word-break: break-all;
}
.kb-level p { margin: 0; color: var(--ink-soft); } /* 5.1:1 on --surface */
.kb-folders-table { min-width: 480px; }
.kb-folders-table td:nth-child(2) { font-family: var(--font); font-size: inherit; max-width: none; }
/* Phase 106 (task 08, D8): the Updated column sits at 3rd (BETWEEN
Documents and Description) — it takes the .docs-table cell defaults
(nowrap text, the table ink: var(--ink) on the wrap's --surface —
13.8:1, the pair recorded at :root). NO new CSS: the one-line clamp
below follows the Description cell, which moved to 4th. */
.kb-folders-table td:nth-child(4) {
white-space: nowrap; /* phase 99 (D1): one line — the clamp's budget is the column width */
min-width: 18rem;
max-width: 44rem;
font-size: 0.88rem;
}
/* Phase 99 (task 01, D1): the Description cell is ONE flex row — the
text span flexes + ellipsizes (the row height is one line, independent
of the description's length), the Edit button stays fixed (flex: 0 0
auto — it never wraps to a second line). The gap replaces the button's
old margin-left (the context-spacing rule below keeps only the level
block's margin-top). The clamp is visual only: the full text stays in
the DOM + on the span's title (hover). */
.kb-desc-cell {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
}
.kb-desc-text {
flex: 1 1 auto;
min-width: 0; /* lets the flex item shrink — what engages the ellipsis (the .sync-label pattern) */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.folder-link {
color: var(--brand-ink); /* 9.0:1 on --surface (the .doc-link pair, row variant) */
text-decoration: none;
border-radius: var(--radius-sm);
}
.folder-link:hover, .folder-link:focus-visible {
background: var(--brand-soft);
text-decoration: underline;
}
/* Phase 167 (task 04, D4): the lazy drill's ONE in-flight row — the
role="status" loading note and the role="alert" failure note (+ the
existing .retry-btn pill) shown in the folders panel of the level
the user is LEAVING: the level never blanks and the breadcrumb never
lies (PLAN §7.4). Tokens only — ink-soft on the wrap's --surface
(the .kb-level p pair, 5.1:1 AA); no animation beyond the house
idiom (the Retry button's :focus-visible is the global rule). */
.kb-level-loading { color: var(--ink-soft); }
/* Image-document rows (phase 122, task 04): the file table's Path cell
carries a FIXED 48px thumbnail box before the path link (image rows
only — text rows keep the pre-phase bare-link cell, no box). The
.kb-doc-path flex wrapper gives the link its own ellipsis budget
(min-width: 0 — the .sync-label pattern) now that the box shares
the cell; object-fit: cover keeps every aspect ratio inside the
square (CSS on .kb-doc-thumb-img). The glyph fallback (a failed
fetch, or a node without a servable image_url) swaps INSIDE the
same fixed box — no layout shift beyond the box, no broken-image
placeholder. The img's alt (the vision description) carries the
accessibility; the glyph is aria-hidden. Phase-08 tokens only. */
.kb-doc-path {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
max-width: 100%;
}
.kb-doc-path .doc-link {
flex: 1 1 auto;
min-width: 0; /* lets the link shrink — what engages the ellipsis */
overflow: hidden;
text-overflow: ellipsis;
}
.kb-doc-thumb {
flex: 0 0 auto;
display: block;
width: 48px;
height: 48px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg);
color: var(--ink-soft); /* the glyph's stroke color (decorative) */
overflow: hidden;
}
.kb-doc-thumb-img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.kb-doc-thumb-glyph {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 9px;
box-sizing: border-box;
}
.kb-doc-thumb-glyph svg {
width: 100%;
height: 100%;
}
/* ---------- KB folder-description editor (phase 97, task 05) ----------
The phase-57 edit affordance on the RAG view's folder descriptions
(the .kb-summary-* family, mirroring the viewer's .doc-summary-*):
the Edit button (a 24px+ target; the [hidden] override beats its
display rule while the editor is open), the inline editor (the
8rem-min textarea + Save/Cancel + the role=status live region).
House dark-tech palette (phase-08 tokens), system fonts, no CDN;
:focus-visible via the global 3px outline rule. No new hue (the
phase-92 monochrome invariant) — every color is a var(). */
.kb-summary-edit {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
/* Phase 156 (task 02, A3): the width support for the label span's
clamp — the flex item is clamped to the cell + shrinkable
(min-width: 0), so a squeezed Description cell ellipsizes the
"Edit" label instead of overflowing the card edge. The level
block's use (the non-flex .kb-level-body) is unaffected: its
natural width never reaches the clamp. */
max-width: 100%;
min-width: 0;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
letter-spacing: 0.02em;
cursor: pointer;
}
.kb-summary-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
.kb-summary-edit[hidden] { display: none; } /* the hidden attr must beat the display above */
/* Context spacing: the row cell's button spacing comes from the
.kb-desc-cell gap (phase 99, task 01 — the margin-left is gone);
the level block's sits below the . */
.kb-level-body .kb-summary-edit { margin-top: 0.5rem; }
.kb-summary-editor {
display: block;
width: 100%;
min-height: 8rem; /* the phase-57 spec */
padding: 0.6rem 0.8rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg); /* inset against the --surface panel / row */
color: var(--ink); /* 16.7:1 on --bg (AA) */
font: inherit;
line-height: 1.5;
resize: vertical;
}
/* Phase 99 (task 01): the open editor fills the .kb-desc-cell flex
wrapper — as a flex item it needs the explicit grow + min-width: 0
(the width: 100% base above still serves the level block's non-flex
.kb-level-body). */
.kb-desc-cell .kb-summary-editor { flex: 1 1 auto; min-width: 0; }
.kb-summary-actions {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.75rem;
}
.kb-summary-save {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0.35rem 0.95rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */
font: inherit;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
}
.kb-summary-save:hover { background: var(--brand-hover); } /* the house hover lightening */
.kb-summary-save:disabled { opacity: 0.6; cursor: default; } /* one PATCH at a time */
.kb-summary-cancel {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0.35rem 0.95rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
}
.kb-summary-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.kb-summary-status {
margin: 0.6rem 0 0;
font-size: 0.85rem;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
}
.kb-summary-status:empty { margin-top: 0; }
/* Phase 98 (task 04, D4): the row-cell "Summary pending" marker — the
muted ink-soft pair (5.1:1 on --surface, AA); text + color, never
color alone. The class sits on the EXISTING description text span,
so the row cell's font-size/line-height apply — the marker must not
change row height (no font/white-space overrides). No italic, no
new hue (the phase-92 monochrome invariant). */
.kb-summary-pending { color: var(--ink-soft); }
/* ---------- Git sources page (phase 35) ----------
/git-sources.html: the admin-only manager for the stored git source
list (add / remove, git-sources table). Same full-width table
language as the Sources page (no skinny single-column list), the
phase-16 gate reused verbatim (.sources-gate classes), and the
phase-27 form-card language for the add form. Every pair reuses the
Phase-08 AA palette: dark ink on brand 5.2:1 (never white on
brand, 3.7:1, fails), brand-ink/brand-soft 6.9:1, err 9.1:1,
ink-soft >=6.9:1. Touch targets >=44px; :focus-visible via the
global 3px outline rule. No filter: blur, no CDN, system fonts. */
.git-sources-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* Phase 174 (owner bug report 2026-10-21): the shell's gap only
separates the gate from this wrapper — every visible card
(page-head, the load-error alert, the env note, the two form
cards, the table card) is a child of THIS div, which was a
plain block, so the cards stacked flush (1px borders touching).
The phase-171 #settings-content idiom: the wrapper itself is
the rhythm column, reusing the shell's own 1.25rem token — no
new spacing value. The hidden children (#git-sources-load-error,
#git-sources-env-note — the env note's CLASS is the singular
git-source-env-note) keep the UA [hidden] display:none inside
a flex parent, so git-sources.js's reveal lifecycle is
untouched; a revealed one joins the rhythm. */
#git-sources-content {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* Add forms — the tuning form's surface as a single row: visible
label + mono location input (git URLs may embed credentials, so the
input is mono) + the brand button; wraps to a column at narrow
widths (the <=640px block below). Phase 49: the archive upload form
(#archive-upload-form, replacing the phase-38 local-directory form)
reuses the git form's card + button rules VERBATIM — one form
language for both; its file input carries its own rules below. */
#git-source-form,
#archive-upload-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 0.9rem 1rem 1rem;
}
#git-source-form:focus-within,
#archive-upload-form:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); }
#git-source-form > label,
#archive-upload-form > label { color: var(--ink); font-weight: 600; white-space: nowrap; }
/* Phase 121 (task 03): the form-label "optional" hint — the muted
ink-soft pair (5.1:1 on the label's surface, AA) at the label's
600 weight relaxed to 400 so the hint reads as secondary (it
qualifies the name, it is not the name); small, inline. */
.field-hint { color: var(--ink-soft); font-weight: 400; font-size: 0.8rem; }
/* #git-source-token (phase 121): the optional masked private-repo
credential — the URL input's treatment VERBATIM (mono, >=44px
target, brand focus); type=password masks the value in display. */
#git-source-url,
#git-source-branch,
#git-source-token {
flex: 1;
min-width: 14rem;
min-height: 44px;
font-family: var(--mono);
font-size: 0.88rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.7rem;
}
#git-source-url::placeholder,
#git-source-branch::placeholder,
#git-source-token::placeholder { color: var(--ink-soft); }
#git-source-url:focus-visible,
#git-source-branch:focus-visible,
#git-source-token:focus-visible { outline-offset: 0; border-color: var(--brand); }
#git-source-add,
#archive-upload-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0.4rem 1.2rem;
border: 0;
border-radius: var(--radius-sm);
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 */
font: inherit;
font-weight: 700;
cursor: pointer;
}
#git-source-add:hover:not(:disabled),
#archive-upload-btn:hover:not(:disabled) { background: var(--brand-hover); }
#git-source-add:disabled,
#archive-upload-btn:disabled { opacity: 0.6; cursor: wait; }
/* Phase 49: the upload form's file control — mono-ish, on-surface, a
>=44px touch target, :focus-visible via the global 3px outline rule
(offset zeroed + brand border, exactly like the git URL input). The
chosen filename renders in mono; the selector button keeps a plain
surface chip. */
#archive-upload-file {
flex: 1;
min-width: 14rem;
min-height: 44px;
font-family: var(--mono);
font-size: 0.88rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.3rem 0.5rem;
}
#archive-upload-file:focus-visible { outline-offset: 0; border-color: var(--brand); }
#archive-upload-file::file-selector-button {
min-height: 34px;
margin-right: 0.6rem;
padding: 0.3rem 0.9rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink); /* 13.8:1 on surface */
font: inherit;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
}
#archive-upload-file::file-selector-button:hover { border-color: var(--brand); color: var(--brand-ink); }
/* The upload's success line (role=status): the .sync-progress-text
soft-ink result-line language ("2 added · 1 pruned") — ink-soft on
surface (>=4.5:1), the dashed hint-box border marks it as a result,
not an error; it drops onto its own row under the input like the
error line. */
#archive-upload-result {
flex-basis: 100%;
margin: 0;
color: var(--ink-soft);
background: var(--surface);
border: 1px dashed var(--line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
}
/* The add form's inline error (role=alert): the err pair (9.1:1);
flex-basis 100% drops it onto its own row under the input. */
.git-source-error {
flex-basis: 100%;
margin: 0;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
}
/* Load failure (role=alert) + the retry action: a failed
GET /api/git-sources must never leave a stuck page. The retry
button keeps the err pair on the err surface (9.1:1). */
.git-source-load-error {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 0.7rem 0.9rem;
font-size: 0.9rem;
font-weight: 600;
}
.git-source-load-error > span { flex: 1; min-width: 12rem; }
#git-sources-retry {
min-height: 44px;
padding: 0.4rem 1rem;
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--err-ink);
font: inherit;
font-weight: 700;
cursor: pointer;
}
#git-sources-retry:hover { background: color-mix(in srgb, var(--err-line) 12%, transparent); }
/* Env-fallback note (from_env: true — the table is empty and the list
is BOR_GIT_SOURCES): the info chip in the theme palette —
brand-soft surface, brand-ink text (6.9:1). */
.git-source-env-note {
margin: 0;
background: var(--brand-soft);
color: var(--brand-ink);
border: 1px solid var(--brand-soft);
border-radius: var(--radius-sm);
padding: 0.6rem 0.9rem;
font-size: 0.88rem;
}
.git-source-env-note code {
font-family: var(--mono);
font-size: 0.85em;
background: var(--surface);
color: var(--brand-ink); /* 8.7:1 on surface */
padding: 0.1em 0.35em;
border-radius: 5px;
}
/* Hint box (role=note): the page-sub styling family — ink-soft on
surface (6.9:1), a dashed border marks it as guidance, not state.
It names the Sync button: the action that clones the listed repos
and prunes the removed ones (the phase scope boundary). */
.git-source-hint {
margin: 0;
color: var(--ink-soft);
background: var(--surface);
border: 1px dashed var(--line);
border-radius: var(--radius-sm);
padding: 0.6rem 0.9rem;
font-size: 0.88rem;
}
/* ---------- Remove confirmation modal (phase 69, task 02) ----------
/git-sources.html: the in-app confirmation that replaces the
native confirm() — the total-removal warning (the row, the
source's indexed documents, and — for git clones and uploaded
archives — the files on the server's disk). The .doc-modal overlay contract:
a fixed full-viewport dim backdrop + a centered panel (z-index
1000, above the sticky header (20) + skip-link (100); NO blur —
the phase-08 no-blur perf anchor), scaled to a compact dialog:
the chat-column width (the --chat-column token — 72rem, the
.container width) or the viewport, whichever is narrower.
Phase-08 tokens only; system fonts; no CDN.
AA pairs: title/copy are --ink on --surface (13.8:1); the source
value is --ink on --bg (16.7:1); the error line is the err pair
(err-ink on err-bg 9.3:1, the err-line border); the destructive
button rides the err token family — the .tuning-delete /
.steering-delete hover convention used as the resting state
(err-ink on err-bg 9.3:1; the hover inverts to dark --bg on
--err-line, 5.2:1 — both AA); Cancel is the ghost ink-soft family
(5.1:1 on --surface). :focus-visible via the global 3px outline
rule (no local suppression — focus lands on Cancel at open and is
visible); both buttons >=44px; no animation (reduced-motion safe
by construction). */
.remove-confirm {
position: fixed;
inset: 0;
z-index: 1000;
display: flex; /* the panel is the only in-flow child — margin: auto centers it */
}
/* Explicit (the global [hidden] rule already wins — the documented,
testable contract for the skeleton). */
.remove-confirm[hidden] { display: none; }
.remove-confirm-backdrop {
position: fixed;
inset: 0;
/* --bg at 82% — the doc-modal dim, no backdrop-filter (no-blur). */
background: color-mix(in srgb, var(--bg) 82%, transparent);
}
.remove-confirm-panel {
/* position:relative lifts the panel above the fixed backdrop
(positioned elements paint over in-flow siblings otherwise). */
position: relative;
z-index: 1;
margin: auto;
width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
}
.remove-confirm-title {
margin: 0 0 0.75rem;
font-size: 1.25rem;
line-height: 1.3;
color: var(--ink);
}
/* The source's value (git URL or local path) — mono, wrapped (a long
URL must not overflow the panel), --ink on --bg (16.7:1). */
.remove-confirm-source {
display: block;
margin: 0 0 0.75rem;
padding: 0.5rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
overflow-wrap: anywhere;
}
.remove-confirm-copy {
margin: 0;
color: var(--ink); /* 13.8:1 on --surface */
}
/* The in-modal failure line (role=alert): the err pair (err-ink on
err-bg 9.3:1, the err-line border) — the .git-source-error banner
language, boxed. */
.remove-confirm-error {
margin: 0.75rem 0 0;
padding: 0.5rem 0.65rem;
color: var(--err-ink);
background: var(--err-bg);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
}
.remove-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
margin-top: 1.1rem;
}
/* The two modal buttons: >=44px targets, the house 3px :focus-visible
via the global outline rule (no local override). */
.remove-confirm-btn {
min-height: 44px;
min-width: 44px;
padding: 0.55rem 1.1rem;
border-radius: var(--radius-sm);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
.remove-confirm-btn:disabled { opacity: 0.5; cursor: wait; }
/* Cancel — the ghost ink-soft family (5.1:1 on --surface), like the
row's Remove / .tuning-edit; the brand pair on hover (6.9:1). */
.remove-confirm-cancel {
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
}
.remove-confirm-cancel:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
/* "Remove source" — the destructive button on the err token family
(the .tuning-delete / .steering-delete convention): err-ink on
err-bg 9.3:1, the err-line border; the hover inverts to dark --bg
on --err-line (5.2:1 — AA). */
.remove-confirm-remove {
border: 1px solid var(--err-line);
background: var(--err-bg);
color: var(--err-ink);
}
.remove-confirm-remove:hover:not(:disabled) {
background: var(--err-line);
color: var(--bg);
}
/* ---------- Per-source ignore-paths editor (phase 89, task 05) ----------
The shell's Sources view: the page-local alertdialog that edits
one source's ignore list (one path per line — the phase-89 A1
prefix rule, stated in plain words in the helper copy). The EXACT
#remove-confirm-dialog overlay contract (phase 69): a fixed
full-viewport dim backdrop + a centered panel (z-index 1000, above
the sticky header (20) + skip-link (100); NO blur — the phase-08
no-blur perf anchor), scaled to the chat-column width (the
--chat-column token — 72rem, the .container width) or the
viewport, whichever is narrower. The box: a VISIBLE block label
(WCAG — never aria-label-only) over a mono textarea (the box is a
data entry, not prose — the var(--mono) stack); the error line is
the err pair (err-ink on err-bg 9.3:1, the err-line border);
Cancel is the ghost ink-soft family (5.1:1 on --surface); Save is
the solid brand family (--bg text on --brand 5.2:1 — the
.new-chat-btn convention; the hover lightens the fill). Both
buttons >=44px; :focus-visible via the global 3px outline rule (no
local suppression — focus lands on Cancel at open and is visible);
no animation (reduced-motion safe by construction). AA pairs: title
/ copy / label are --ink (or ink-soft) on --surface (13.8:1 /
5.1:1); the source value is --ink on --bg (16.7:1). Phase-08
tokens only; system fonts; no CDN. */
.ignore-editor {
position: fixed;
inset: 0;
z-index: 1000;
display: flex; /* the panel is the only in-flow child — margin: auto centers it */
}
/* Explicit (the global [hidden] rule already wins — the documented,
testable contract for the skeleton). */
.ignore-editor[hidden] { display: none; }
.ignore-editor-backdrop {
position: fixed;
inset: 0;
/* --bg at 82% — the doc-modal dim, no backdrop-filter (no-blur). */
background: color-mix(in srgb, var(--bg) 82%, transparent);
}
.ignore-editor-panel {
/* position:relative lifts the panel above the fixed backdrop
(positioned elements paint over in-flow siblings otherwise). */
position: relative;
z-index: 1;
margin: auto;
width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
}
.ignore-editor-title {
margin: 0 0 0.75rem;
font-size: 1.25rem;
line-height: 1.3;
color: var(--ink);
}
/* The source's value (git URL or local path) — mono, wrapped (a long
URL must not overflow the panel), --ink on --bg (16.7:1). */
.ignore-editor-source {
display: block;
margin: 0 0 0.75rem;
padding: 0.5rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
overflow-wrap: anywhere;
}
.ignore-editor-copy {
margin: 0 0 1rem;
color: var(--ink); /* 13.8:1 on --surface */
}
.ignore-editor-copy code { font-family: var(--mono); font-size: 0.85em; }
/* Visible block label (WCAG — the textarea is NEVER labelled only via
aria-label); ink-soft on --surface (5.1:1). */
.ignore-editor-label {
display: block;
margin-bottom: 0.35rem;
color: var(--ink-soft);
font-size: 0.88rem;
font-weight: 600;
}
/* The box: full panel width, mono (the var(--mono) stack), theme-
consistent border on --bg; the focus-visible ring is the global
3px outline (no local suppression); vertical resize for long
lists (up to 200 entries, A4). */
.ignore-editor-textarea {
display: block;
width: 100%;
min-height: 9rem;
padding: 0.55rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
resize: vertical;
}
/* Phase 121 (task 03): the editor's masked token field — the
textarea's box language (full panel width, --bg fill, the line
border, mono) at the 44px touch floor; the label's top margin keeps
the field pair off the textarea. type=password + autocomplete=off
live in the markup (a PAT is not a site credential — no browser
save offer); the focus ring is the global 3px outline rule. */
.ignore-editor-token-label { margin-top: 0.9rem; }
.ignore-editor-token {
display: block;
width: 100%;
min-height: 44px;
padding: 0.55rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
}
/* Phase 135 (task 04): the editor's branch field — the token field's
box language (full panel width, --bg fill, the line border, mono)
at the 44px touch floor; type=text (a branch is a name, not a
credential — no masking) + autocomplete=off live in the markup, and
the label reuses .ignore-editor-label (the source line's 0.75rem
bottom margin is the field pair's top gap — no extra margin class).
The muted hint line carries the .field-hint values (ink-soft, 400,
0.8rem — the token field's hint styling) as a standalone
; the
bottom margin is the gap to the copy line (which has no top margin).
Git rows only — git-sources.js hides the group for local rows. */
#ignore-editor-branch {
display: block;
width: 100%;
min-height: 44px;
padding: 0.55rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
}
.ignore-editor-branch-hint {
margin: 0.35rem 0 1rem;
color: var(--ink-soft);
font-size: 0.8rem;
}
/* The in-dialog failure line (role=alert): the err pair (err-ink on
err-bg 9.3:1, the err-line border) — the .remove-confirm-error
language. */
.ignore-editor-error {
margin: 0.75rem 0 0;
padding: 0.5rem 0.65rem;
color: var(--err-ink);
background: var(--err-bg);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
}
.ignore-editor-actions {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
margin-top: 1.1rem;
}
/* The two dialog buttons: >=44px targets, the house 3px :focus-visible
via the global outline rule (no local override). */
.ignore-editor-btn {
min-height: 44px;
min-width: 44px;
padding: 0.55rem 1.1rem;
border-radius: var(--radius-sm);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
.ignore-editor-btn:disabled { opacity: 0.5; cursor: wait; }
/* Cancel — the ghost ink-soft family (5.1:1 on --surface), like the
remove dialog's Cancel; the brand pair on hover (12.4:1 on
brand-soft). */
.ignore-editor-cancel {
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
}
.ignore-editor-cancel:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
/* "Save" — the solid brand family (the .new-chat-btn convention):
--bg text on --brand (5.2:1 — AA); the hover lightens the brand
fill (the .new-chat-btn hover pair). */
.ignore-editor-save {
border: 0;
background: var(--brand);
color: var(--bg);
}
.ignore-editor-save:hover:not(:disabled) { background: var(--brand-hover); }
/* The list: the Sources page's table pattern — full width in the
72rem frame. The URL column never wraps or ellipsizes: long
URLs, credentials included, scroll the wrapper instead of
truncating (the .git-sources-table 560px min-width keeps the
in-wrapper horizontal scroll). Phase 156 (task 01): the id
card's duplicate background/border/radius/shadow/overflow
declarations were REMOVED — the wrapper carries
class="table-wrap", so it inherits the base card + the
bounded-panel max-height + the sticky rule (the one-rule
contract: no divergent unbounded override). */
.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 {
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* 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;
/* Phase 156 (task 02, A3): the width support for the "Remove"
label span's clamp (same rationale as .git-source-hidden
above). */
max-width: 100%;
min-height: 44px;
min-width: 44px;
flex: 0 0 auto;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.git-source-remove svg { width: 14px; height: 14px; display: block; }
.git-source-remove:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.git-source-remove:disabled { opacity: 0.5; cursor: wait; }
/* Phase 89: the per-row "Ignore paths" trigger (makeRow — left of
Remove): the .git-source-remove idiom (same size/spacing, >=44px
target) in a NEUTRAL secondary fill — it must read distinct from
the destructive Remove (text label included, never icon-only; the
hover takes the brand-soft pair, 12.4:1 — Remove hovers to the err
pair, 9.3:1). */
.git-source-ignore {
display: inline-flex;
align-items: center;
justify-content: center;
/* Phase 156 (task 02, A3): the width support for the "Ignore
paths" label span's clamp (same rationale as .git-source-hidden
above). */
max-width: 100%;
min-height: 44px;
min-width: 44px;
flex: 0 0 auto;
padding: 0.35rem 0.7rem;
margin-right: 0.4rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.git-source-ignore:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
/* Phase 89: the "N ignored" count tag — next to the location
in the Source cell (TEXT + a distinct background, never color
alone — WCAG 1.4.1): --ink on --bg (16.7:1) inside the --surface
card; the line border keeps the chip legible when the row hover
swaps the cell onto --bg too. */
.git-source-ignore-count {
display: inline-block;
margin-left: 0.55rem;
padding: 0.08rem 0.5rem;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
color: var(--ink);
background: var(--bg);
white-space: nowrap;
}
/* Phase 105 (A5): the per-row "Hidden" checkbox — the actions
cell, LEFT of the "Ignore paths" button (JS builds it,
git-sources.js makeRow). A native labeled checkbox: the
visible "Hidden" text + the checkbox's own aria-label
(full source value). ~44px hit height matches the action
buttons; :focus-visible via the GLOBAL rule (L146) — no
per-control ring needed. */
.git-source-hidden {
display: inline-flex;
align-items: center;
gap: 0.4rem;
/* Phase 156 (task 02, A3): the width support for the "Hidden"
label span's clamp (the label is an inline-flex child of the
cell — clamped to the cell so the span can ellipsize in a
squeezed column instead of overflowing the card edge). */
max-width: 100%;
height: 44px;
padding: 0 0.5rem;
font-size: 0.8rem;
color: var(--ink);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
/* --brand checkbox fill: the native check renders in --bg on
--brand — VERIFIED (phase 105): built-in dark theme
#0f0a0a on #f43f5e = 5.2:1 (the house brand pairing;
measured 5.35:1 — WCAG AA ≥4.5:1); every theme-tab-saved
palette is gated by the tab's eight-pair contrast check
(theme.js PAIRS — "bg" on "brand" at AA_MIN 4.5), so the
checked-state ratio stays AA in all themes. */
.git-source-hidden input[type="checkbox"] {
width: 1.05rem;
height: 1.05rem;
margin: 0;
accent-color: var(--brand);
cursor: pointer;
}
.git-source-hidden:disabled { opacity: 0.5; cursor: wait; }
/* Phase 105 (A5): the "hidden on" state tag — a copy of the
.git-source-ignore-count idiom (TEXT + background, never
color alone — WCAG 1.4.1: --ink on --bg 16.7:1). Phase 135 (A6):
the "branch: " state tag joins the same rule — stored git
rows with a branch only, the text IS the signal.
Phase 191 (task 03): the TWO new tags of the name/label/tags row join
the SAME rule — .source-label-state ("named", beside the mono identity)
and .source-label-tag (one chip of the Tags token input). That is the
reuse the phase asks for: the chips are the house tag, not a new
widget, so no tag-input styling is added anywhere below. */
.git-source-hidden-count,
.git-source-branch-count,
.source-label-state,
.source-label-tag {
display: inline-block;
margin-left: 0.55rem;
padding: 0.08rem 0.5rem;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
color: var(--ink);
background: var(--bg);
white-space: nowrap;
}
/* Env-fallback rows carry no Remove (nothing is stored to remove) —
the tag says where the row comes from (brand pair, 6.9:1). */
.git-source-env-tag {
color: var(--brand-ink);
background: var(--brand-soft);
border-radius: 999px;
padding: 0.2rem 0.6rem;
font-size: 0.78rem;
font-weight: 600;
}
/* Empty state: the tuning page's quiet centered line at full table
width — no stored rows AND no env fallback to show. Phase 191 (task
03): the name/label/tags list shares the line verbatim (its own
no-sources case, the same quiet card). */
.git-sources-empty,
.source-labels-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);
}
/* ---------- Source names, labels and tags (phase 191, task 03) ----------
The Sources page's SECOND list: every source the knowledge base knows
(git, local, upload — the union the phase-191 catalogue GET returns),
each with the optional name / label / tags the model is told about
(D1: an ALIAS — the mono identity cell is read-only text, nothing here
renames a document, a citation or a locator).
Nothing new is invented: the card is the phase-156 .table-wrap (bounded
panel + sticky header, inherited by the class alone), the identity cell
is the .git-source-url-cell mono, the state tag and the tag CHIPS are
the phase-89 tag rule above, the field boxes are the .ignore-editor-
token / #ignore-editor-branch language at the 44px floor, Save is the
page's brand pill, Reset and the row's toggle are the neutral
.git-source-ignore language, and the dirty mark is the phase-168
the phase-168 dirty-row stripe. PALETTE VARIABLES ONLY — no new hue
(every pair is a pair this page already measures: ink 15.3:1 and
ink-soft 8.2:1 on --surface, brand-ink on brand-soft 12.4:1, the err
pair 9.3:1, --bg ink on --brand 5.2:1); :focus-visible rides the global
3px outline rule; no CDN, no blur. The editor expands UNDER its own row
(one row open at a time keeps a 27-source list scannable) and its three
fields stack in a single column below 60rem instead of pushing the card
into a horizontal scroll at 375px. */
.source-labels {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.source-labels-title {
margin: 0;
font-size: 1.25rem;
line-height: 1.3;
color: var(--ink);
}
/* The alias note (the contract in the owner's words) — the page-sub
language, ink-soft on the page canvas (8.2:1 on --surface). */
.source-labels-sub {
margin: 0;
max-width: 60rem;
color: var(--ink-soft);
font-size: 0.88rem;
}
/* The list's own load failure (role=alert) — the .git-source-error
pair (err-ink on err-bg 9.3:1). */
.source-labels-error {
margin: 0;
padding: 0.45rem 0.8rem;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
font-size: 0.85rem;
font-weight: 600;
}
/* No min-width on purpose: the editor column has to SHRINK (the fields
stack), so this table never scrolls the card sideways at 375px — the
panel still bounds vertically (.table-wrap). */
.source-labels-table {
width: 100%;
border-collapse: collapse;
font-size: 0.93rem;
}
.source-labels-table th, .source-labels-table td {
text-align: left;
vertical-align: top;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
}
.source-labels-table th {
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.source-labels-table tbody tr:last-child td { border-bottom: 0; }
/* The identity cell: the derived name, mono like the sources table's
location cell, wrapping (overflow-wrap) instead of widening the card —
it is text, never an input (D1/A1). */
.source-labels-table td.source-label-id-cell {
font-family: var(--mono);
font-size: 0.82rem;
overflow-wrap: anywhere;
}
.source-labels-table td.source-label-id-cell code { font-family: inherit; }
/* The dirty row mark — the phase-168 dirty-row stripe (the brand edge is
beside the row's own "unsaved changes" TEXT, so the state is never color
alone — WCAG 1.4.1). The literal is NOT written in this comment: the
phase-168 pin (test_settings_frontend_view.py) regex-matches that
selector to its own rule and a comment mention would steal the match. */
.source-label-row.is-dirty > td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
/* The collapsed row's read-out of the stored label (name · label · tags).
textContent-only (a label is owner text — the XSS contract). */
.source-label-summary {
margin: 0 0 0.45rem;
color: var(--ink-soft);
font-size: 0.85rem;
overflow-wrap: anywhere;
}
/* The row's expand trigger — the .git-source-ignore language (neutral
secondary fill, >=44px, hovers to the brand pair 12.4:1). */
.source-label-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
max-width: 100%;
min-height: 44px;
min-width: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.source-label-toggle:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
/* The expanded editor panel — the row's own form card. The fields go in
one column narrow, three across from 60rem (the house grid). */
.source-label-editor {
display: grid;
gap: 0.9rem;
margin-top: 0.75rem;
padding: 0.9rem;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
}
@media (min-width: 60rem) {
.source-label-fields { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.source-label-fields {
display: grid;
gap: 0.75rem;
}
.source-label-field {
display: block;
min-width: 0;
}
/* The field label — the .ignore-editor-label language (ink-soft 8.2:1 on
the panel, 600 weight, the hint span the .field-hint secondary read). */
.source-label-field-label {
display: block;
margin-bottom: 0.35rem;
color: var(--ink-soft);
font-size: 0.88rem;
font-weight: 600;
}
/* The field box — the .ignore-editor-token / #ignore-editor-branch box
language VERBATIM (full width, --bg… panel fill, the line border, mono)
at the 44px touch floor; the global [hidden] rule and the global
:focus-visible outline cover the rest. */
.source-label-input {
display: block;
width: 100%;
min-height: 44px;
padding: 0.55rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
}
/* The Tags token input's chip list — the LAYOUT only (the chips
themselves are the shared tag rule above, so the widget adds no tag
styling). list-style is dropped, so role="list" stays in the markup
(the VoiceOver list-semantics idiom the house uses). */
.source-label-tags {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin: 0 0 0.45rem;
padding: 0;
list-style: none;
}
.source-label-tags .source-label-tag { margin-left: 0; }
.source-label-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
}
/* Save — the page's brand pill at the row scale (--bg ink on --brand,
5.2:1); disabled until the row is dirty, and again while the PUT is
out (§7.4 — the row's own in-flight state). */
.source-label-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;
}
.source-label-save:hover:not(:disabled) { background: var(--brand-hover); }
.source-label-save:disabled { opacity: 0.6; cursor: default; }
/* Reset — the neutral secondary button (the .git-source-ignore language):
it clears a STORED label, it is not the destructive row Remove. */
.source-label-reset {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0.35rem 0.9rem;
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;
cursor: pointer;
}
.source-label-reset:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
.source-label-reset:disabled { opacity: 0.5; cursor: wait; }
/* The §7.4 save-state line (role=status): "Saving…" → "saved" / the
one-line error note. The hint (unsaved) is the muted italic; the error
takes the err pair (9.3:1) so the note reads as a failure, not as a
label — TEXT carries it either way, never color alone. */
.source-label-status {
color: var(--ink-soft);
font-size: 0.82rem;
}
.source-label-status.is-hint { font-style: italic; }
.source-label-status.is-error {
padding: 0.35rem 0.6rem;
color: var(--err-ink);
background: var(--err-bg);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
font-weight: 600;
}
/* ---------- History refresh button (phase 77 task 03, TODO.md L3) ----------
The #view-history page-head is a flex row: the title block left, the
actions slot right (the row wraps below 640px). Scoped to
#view-history — the other four views' page-heads are untouched.
The button reuses the .new-chat-btn visual language: solid brand
pill, --bg text on --brand (5.2:1, WCAG AA >=4.5:1), borderless,
>=44px target, hover lightens the brand fill, focus-visible via the
global 3px rule. The phase-46 auth-link convention: the label is
visible >=640px, the glyph is the whole control below (the button's
aria-label keeps the accessible name in both). */
#view-history .page-head {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem 1.5rem;
}
#view-history .page-head .page-head-title { min-width: 0; }
.history-refresh, .history-pager-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 0;
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 700;
font-size: 0.95rem;
white-space: nowrap;
cursor: pointer;
flex-shrink: 0;
}
.history-refresh:hover, .history-pager-btn:hover { background: var(--brand-hover); color: var(--bg); }
.history-refresh:disabled, .history-pager-btn:disabled { opacity: 0.6; cursor: wait; }
/* The refresh glyph is hidden on desktop (the label carries the
pill); below 640px it joins the visible label in the full-width
pill (the <=640 block — the icon-only squeeze is gone). The
phase-175 pager buttons join the rule (the selector union — the
same glyph language: visible label at every width, the glyph is
the mobile companion). */
.history-refresh svg, .history-pager-btn svg { width: 16px; height: 16px; display: none; }
/* ---------- History pager (phase 175, task 02 — TODO.md L3) ----------
"Need pagination on the history page - it gets long fast" (owner
2026-10-10). Root cause: the saved-chat list used to fetch and
render EVERY row; the server now ships a 25-row page + the full
count, and this row is the walk control — Prev / "Page N of M" /
Next, sitting UNDER the table card (index.html, phase 175).
Token reuse (B3): the buttons join the .history-refresh pill
through the selector unions above (the solid brand pill, the
:disabled opacity, the svg sizing — NO new palette variable), and
the row's gap is the page's OWN 0.75rem (the #view-history
.page-head row gap — no new spacing token). Hidden by default
(the markup ships `hidden`): history.js's updatePager() reveals
the row only while `total > PAGE_SIZE` (A4 — no dead "Page 1 of
1" control; it disappears with the empty state). The status span
needs no rule beyond the inherited text. Below 640px the row
stacks (the .settings-actions idiom) and the pills go full width
(the .history-refresh mobile idiom — the ≤640px block below). */
.history-pager {
display: flex;
align-items: center;
gap: 0.75rem;
}
/* ---------- History page (phase 50) ----------
/history.html: the admin-only saved-chats list (task 04). The
FULL-WIDTH table in the 72rem frame (AGENTS.md rule 5 — no skinny
single-column list), the same table language as the Sources page
(the sources-table family: --line hairlines, the brand-soft-tinted
thead, row hover, the scrollable .table-wrap). Every pair reuses
the Phase-08 AA palette: brand-ink on brand-soft 6.9:1, ink-soft
>=6.9:1, err 9.1:1. :focus-visible via the global 3px outline
rule. No CDN, system fonts. */
.history-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* Action feedback line (role=status): the .sync-progress-text mono /
soft-ink language — ink-soft on surface (>=4.5:1), small mono; the
min-height holds the layout so a delete's line never reflows the
table. */
.history-status {
display: block;
min-height: 1.2em;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
}
/* The table is FULL-WIDTH (AGENTS.md rule 5): width 100% inside the
standard .container; the .table-wrap card + its horizontal scroll
cover narrow widths (the phase-07 responsive contract). */
.history-table {
width: 100%;
border-collapse: collapse;
min-width: 640px;
font-size: 0.93rem;
}
.history-table th, .history-table td {
text-align: left;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.history-table th {
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.history-table tbody tr:hover { background: var(--bg); }
.history-table tbody tr:last-child td { border-bottom: 0; }
/* Title cell: the Open link — the accent link (brand-ink on surface
6.9:1); the column ellipsizes, the full title sits in the title
attribute (on the cell AND the link). */
.history-title-cell {
max-width: 34rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-title-link {
color: var(--brand-ink);
font-weight: 600;
text-decoration: none;
}
.history-title-link:hover { text-decoration: underline; }
.history-title-link:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Messages: the mono numeric readout (ink-soft >=6.9:1). */
.history-count-cell { font-family: var(--mono); color: var(--ink-soft); }
/* Updated: locale date+time (ink-soft), the full ISO in the title
attribute (history.js). */
.history-updated-cell { color: var(--ink-soft); white-space: nowrap; }
/* Stale marker (phase 53, task 04): the READ-ONLY staleness badge on
rows saved before the last KB-changing sync (the Regenerate action
lives on the chat-page banner — the marker is a pill, never a
control). The rose family, i.e. the Stop-treatment tokens, so
"stale" reads in the same visual language as the in-flight control:
err-ink on err-bg ≈9.3:1 (≥4.5:1 on --surface too), err-line
border — theme tokens, AA in the palette as a whole. Fresh rows'
em-dash rides the cell's ink-soft (5.1:1 on --surface). */
.history-stale-cell { color: var(--ink-soft); white-space: nowrap; }
.stale-pill {
display: inline-block;
padding: 0.15rem 0.55rem;
border: 1px solid var(--err-line);
border-radius: 999px;
background: var(--err-bg);
color: var(--err-ink);
font-size: 0.75rem;
font-weight: 700;
line-height: 1.45;
white-space: nowrap;
}
/* Actions: the Delete ghost button (the tuning row-action language)
+ the inline two-step confirm pair (phase 50 task 04). */
/* Phase 156 (task 02, A3): the width support for the action
labels' clamp — both wrappers are clamped to the cell (max-width
100% of the td's content box), and the flex-item buttons are
shrinkable (min-width: 0), so a squeezed Actions/Share column
ellipsizes the label instead of the card edge hard-cutting it.
Mobile (≤640px) is unaffected in kind: the flex-wrap there wraps
the buttons onto their own lines before the clamp engages. */
.history-actions { display: inline-flex; align-items: center; gap: 0.4rem; max-width: 100%; }
.history-delete {
min-height: 44px;
min-width: 0;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-delete:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.history-delete:disabled { opacity: 0.5; cursor: wait; }
.history-confirm-text { color: var(--err-ink); font-size: 0.82rem; font-weight: 700; white-space: nowrap; }
/* Yes: the error-rose treatment (err-ink on err-bg 9.1:1, err-line
border); No: the ghost (transparent, --line border). */
.history-confirm-yes {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
background: var(--err-bg);
color: var(--err-ink);
font: inherit;
font-weight: 700;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-confirm-yes:hover:not(:disabled) { background: color-mix(in srgb, var(--err-line) 18%, transparent); }
.history-confirm-yes:disabled { opacity: 0.6; cursor: wait; }
.history-confirm-no {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-confirm-no:hover { background: var(--brand-soft); color: var(--brand-ink); }
/* Share column (phase 51, owner-locked 2026-08-29): the Tune/Retry-
family ghost buttons — Create link (unshared) and Copy (shared);
Unshare is the .history-unshare ghost and its two-step confirm reuses
the .history-confirm-* pair CSS above (the phase-50 pattern). The
row-action language of .history-delete: --line border, transparent
fill, ink-soft (>=5.1:1), ≥44px comfortable target, focus-visible
via the global 3px rule; hover takes the brand pair (6.9:1). */
.history-share { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: nowrap; max-width: 100%; }
.history-share-cell { white-space: nowrap; }
.history-share-create,
.history-share-copy,
.history-unshare {
min-height: 44px;
min-width: 0; /* phase 156 (task 02, A3) — the shrinkable flex item (the clamp support above) */
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-share-create:hover:not(:disabled),
.history-share-copy:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); }
.history-unshare:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.history-share-create:disabled,
.history-share-copy:disabled,
.history-unshare:disabled { opacity: 0.5; cursor: wait; }
/* The share link's inline fallback field (phase 51, owner-locked): a
non-secure (http) origin rejects the clipboard, so the full URL is
offered as an input-like that selects itself on focus — click or
Tab, then Ctrl/Cmd+C. Mono (the URL is data), surface fill, --line
border; truncates with an ellipsis at narrow widths (the full URL is
the title + the text selection). Ink on surface ≈13.8:1. */
.share-link-fallback {
display: inline-block;
max-width: 14rem;
padding: 0.35rem 0.55rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font-family: var(--mono);
font-size: 0.78rem;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.share-link-fallback:hover { border-color: var(--brand); }
.share-link-fallback:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Empty-state row: the muted centered message at full table width
(the .git-sources-empty language, inline in the table). */
.history-empty-row td {
padding: 2.25rem 1rem;
text-align: center;
color: var(--ink-soft);
font-style: italic;
}
/* ---------- Tokens view (phase 79 task 06) ----------
The admin-issued access tokens (generate · list · revoke). The
shell's standard frame (the .container): the page-head (h1 + sub),
the #tokens-gate (the #history-gate / .sources-gate language), the
create row (label + Generate), the shown-once block (the mono
field + Copy), and the FULL-WIDTH table (AGENTS.md rule 5 — the
.history-table language: --line hairlines, the brand-soft-tinted
thead, row hover, the scrollable .table-wrap). Every pair reuses
the Phase-08 AA palette: brand-ink on brand-soft 6.9:1, ink-soft
>=5.1:1, err 9.3:1. :focus-visible via the global 3px outline
rule. No CDN, system fonts. */
.tokens-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* Action feedback line (role=status): the .history-status shape —
the min-height holds the layout so a line never reflows the table. */
.tokens-status {
display: block;
min-height: 1.2em;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
}
/* The create row: label input + Generate — flex, wraps below 640px
(the button drops under the full-width input). */
.token-create {
display: flex;
align-items: center;
gap: 0.6rem;
}
#token-label {
flex: 1;
min-width: 0;
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font: inherit;
font-size: 0.93rem;
}
#token-label:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Generate: the .history-refresh brand pill language — the solid
brand fill (--bg text on --brand 5.2:1, AA), the ≥44px target, the
lightened hover fill, the dimmed :disabled (the in-flight state),
the global :focus-visible ring. */
.token-generate {
min-height: 44px;
padding: 0.4rem 1.1rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
.token-generate:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); }
.token-generate:disabled { opacity: 0.6; cursor: wait; }
/* The shown-once block (owner-locked A4): a quiet brand-soft card
around the "shown once" line + the mono read-only field + Copy —
the token reads as the app's own credential surface (brand-ink on
brand-soft 12.4:1; ink on the field 13.8:1). */
.token-once {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.9rem 1rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--brand-soft);
}
.token-once-copy {
margin: 0;
color: var(--brand-ink);
font-size: 0.85rem;
font-weight: 600;
}
.token-once-row {
display: flex;
align-items: center;
gap: 0.6rem;
}
/* The mono read-only field: the token is DATA — mono, surface fill,
--line border; it truncates with an ellipsis at narrow widths (the
full value is the text selection — the inline copy fallback). */
#token-once-value {
flex: 1;
min-width: 0;
min-height: 44px;
padding: 0.35rem 0.55rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font-family: var(--mono);
font-size: 0.78rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#token-once-value:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Copy: the Tune/Retry-family ghost button (the .history-share-copy
language — --line border, transparent fill, ink-soft, ≥44px, the
brand hover pair). */
#token-once-copy {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
#token-once-copy:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
/* The table is FULL-WIDTH (AGENTS.md rule 5): width 100% inside the
standard .container; the .table-wrap card + its horizontal scroll
cover narrow widths (the phase-07 responsive contract). The
.history-table language, verbatim. */
.tokens-table {
width: 100%;
border-collapse: collapse;
min-width: 640px;
font-size: 0.93rem;
}
.tokens-table th, .tokens-table td {
text-align: left;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.tokens-table th {
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.tokens-table tbody tr:hover { background: var(--bg); }
.tokens-table tbody tr:last-child td { border-bottom: 0; }
/* Label: the hand-out name (the column ellipsizes, the full label
sits in the title attribute — tokens.js). */
.tokens-label-cell {
max-width: 20rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
}
/* Created / Last used: locale date+time (ink-soft), the full ISO in
the title attribute; "never" for the not-yet-used stamp. */
.tokens-date-cell { color: var(--ink-soft); white-space: nowrap; }
/* Status: the Active em-dash rides the cell's ink-soft (5.1:1 on
--surface); the Revoked marker reuses the .stale-pill rose family
(err-ink on err-bg ≈9.3:1, err-line border — the stale-pill visual
language). */
.tokens-status-cell { color: var(--ink-soft); white-space: nowrap; }
/* Actions: the Revoke ghost button (the .history-delete language —
--line border, transparent fill, ink-soft, ≥44px target) + the
inline two-step confirm (the .history-confirm-* pair CSS above —
the phase-50 pattern). */
.tokens-actions { display: inline-flex; align-items: center; gap: 0.4rem; }
.token-revoke {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.token-revoke:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.token-revoke:disabled { opacity: 0.5; cursor: wait; }
/* Regenerate (phase 101 task 03, D2): the .token-revoke's structural
twin — the SAME size / border / radius / focus, the transparent
fill + ink-soft text — but the NEUTRAL action's hover
(brand-soft / brand-ink), NOT the revoke's error hover: a
rotation is a hand-out of a successor, not a deletion (no new
hue — the phase-92 monochrome invariant). The confirm pair
reuses the .history-confirm-* classes unchanged (the pair CSS
above). :disabled mirrors the revoke button's rule (the
in-flight state, while the rotation POST is out). */
.token-regenerate {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.token-regenerate:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); }
.token-regenerate:disabled { opacity: 0.5; cursor: wait; }
/* Empty-state + search no-match rows (phase 101): the muted centered
message at full table width (the .history-empty-row language,
inline in the table) — CLASS-based, so both the active table's
#tokens-empty-row / #tokens-no-match-row and the revoked table's
#tokens-revoked-no-match-row share it. */
.tokens-empty-row td {
padding: 2.25rem 1rem;
text-align: center;
color: var(--ink-soft);
font-style: italic;
}
/* Phase 101 (task 02, D4): the per-table live label search — the
house input surface (the #token-label / archive-upload input
family: --line hairline, --surface fill, ink text — AA pairs, no
new hue), full width, the ≥44px target, the global :focus-visible
ring (no custom focus rule — the house pattern). */
.token-search {
width: 100%;
min-height: 44px;
padding: 0.55rem 0.8rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font: inherit;
font-size: 0.9rem;
}
/* The revoked section's sub-heading (phase 101 D1): the phase-97
.kb-level h2 voice — mono, 1rem, brand-ink (the path voice, AA on
the page background). */
.tokens-revoked-heading {
margin: 1.5rem 0 0.5rem;
font-family: var(--mono);
font-size: 1rem;
color: var(--brand-ink);
}
/* ---------- Theme view (phase 91, tasks 04 + 05) ----------
The shell's seventh view (#view-theme): the admin palette + branding
editor (task 05). The full 72rem container width (owner
instruction 2026-09-12 — the phase-91 form-column cap is
superseded), the
form card (the #tune-form language: surface fill, --line hairline,
radius, shadow), the fieldset groups (Branding / Palette) with the
.theme-note sub-copy, the 3-column (desktop) / 1-column (mobile)
palette grid with the color swatch inputs sized for touch (44px —
the label above is the second tap affordance), and the Save/Reset
row (Save = the brand pill family, --bg ink on --brand 5.2:1 AA;
Reset = the ghost button family, --line border; the §7.4
disabled-while-in-flight look is the :disabled pair below). The
feedback lines: error/result are the house role=status/alert line
languages; #theme-contrast (the WCAG warning) uses the --err-*
STATE family — states are semantic colors, never themed from the
tab (B3). Every pair reuses the Phase-08 AA palette; :focus-visible
via the global 3px outline rule. No CDN, system fonts. */
.theme-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* The static form — one card holding the two fieldset groups + the
actions row (the #tune-form card language). */
#theme-form {
display: flex;
flex-direction: column;
gap: 1rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1rem 1.1rem 1.1rem;
}
/* The fieldset groups: reset the UA border (the card IS the group's
frame), the legend rides the group's title line (the
.tuning-panel-title language — ink, 1rem, 700). */
.theme-group {
margin: 0;
padding: 0;
border: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.theme-group-title {
padding: 0;
font-size: 1rem;
font-weight: 700;
color: var(--ink);
}
/* The branding text inputs: the #token-label language (surface is
already the card fill — transparent field, --line hairline, ≥44px
target, the global :focus-visible ring). */
#theme-form fieldset label {
font-size: 0.88rem;
font-weight: 600;
color: var(--ink-soft);
}
#theme-form fieldset input[type="text"] {
width: 100%;
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink);
font: inherit;
font-size: 0.93rem;
}
/* The palette note under the branding legend (task 05): the strings
apply on the next page load (B4) — the live preview covers the
palette only. */
.theme-note {
margin: 0;
font-size: 0.8rem;
line-height: 1.35;
color: var(--ink-soft);
}
/* The color-input grid (task 05): the 8 swatches in 3 columns
(3 + 3 + 2 rows) — the cell is a .theme-color (label above the
swatch); 1 column at the mobile breakpoint (below). */
.theme-colors {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.6rem 0.9rem;
}
.theme-color {
display: flex;
flex-direction: column;
gap: 0.3rem;
min-width: 0;
}
.theme-color label {
font-size: 0.8rem;
font-weight: 600;
color: var(--ink-soft);
line-height: 1.25;
}
.theme-color input[type="color"] {
/* Touch-sized swatch (task 05): 44px tall (the WCAG 2.5.8 target —
the label above is the second tap affordance). */
inline-size: 56px;
block-size: 44px;
padding: 3px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg);
cursor: pointer;
}
/* Save (primary) + Reset (secondary): the pill + ghost families
(.history-refresh / .token-revoke languages — the global
:focus-visible ring applies, no button-scoped focus override). */
.theme-actions {
display: flex;
gap: 0.6rem;
margin-top: 0.2rem;
}
#theme-save {
min-height: 44px;
padding: 0.4rem 1.2rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 (never white on brand) */
font: inherit;
font-weight: 700;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
#theme-save:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); }
#theme-save:disabled { opacity: 0.6; cursor: wait; }
.theme-reset {
min-height: 44px;
padding: 0.4rem 1rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
.theme-reset:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
.theme-reset:disabled { opacity: 0.6; cursor: wait; }
/* The three feedback lines (task 05): error (role=alert — the
server's 422 detail) and result (role=status) are the house line
languages; min-height holds the layout so a line never reflows the
form. #theme-contrast (the WCAG warning, task 05) takes the
.tuning-error box treatment in the --err-* STATE family — states
are semantic colors, never themed from the tab (B3): a failing
pair reads as a warning and the box is warning-only (Save is never
disabled by it). */
.theme-error,
.theme-result,
.theme-contrast {
display: block;
margin: 0;
min-height: 1.2em;
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
}
.theme-error { color: var(--err-ink); }
.theme-result { color: var(--ok-ink); }
.theme-contrast {
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.8rem;
}
/* ---------- Settings view (phase 168) ----------
The admin settings panel: ONE .table-wrap card (the phase-156
bounded scroll region — max-height: min(70vh, 64rem) — and the
sticky thead via the .table-wrap rule) holding the 75-row
catalogue (grouped, type-correct controls, literal-text source
badges — B5: text + color, never color alone), the client-side
search row, and the Save / Clear-all action row. Every color is
an EXISTING AA token (B3: no new palette variable, no new :root
token — the phase-156 rule): the badges are TEXT and the color
is decoration. Nothing animates, so prefers-reduced-motion needs
no new rule. */
.settings-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* Phase 171 (task 01; TODO L4): the #settings-content block —
the disabled banner, the status + error lines, the search row,
the .table-wrap card, and the Save / Clear-all action row —
stacks with ONE uniform gap: the shell's own 1.25rem
(ASSUMPTION A4: no new spacing value; the 640px mobile
stacking composes with it unchanged). The gap kills both
layout complaints at once: the search box clears the table
card (the "overlap") and the action row separates from the
table (the "smushed"). NO per-element margins: the gap is the
ONLY spacing source; the #settings-status / .settings-error
min-heights already reserve their lines (the gap is additive
and uniform). */
#settings-content {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* The catalogue: FULL-WIDTH (AGENTS.md rule 5 — the .git-sources-
table language: no skinny list), one cell per column, the
.table-wrap card + its bounded scroll + the sticky thead carry
the phase-156 contract. The ≤640px mobile block (phase 176) raises
the floor to 720px so the 13rem Value column stays readable on a
phone (the card's horizontal scroll absorbs it; this rule keeps
560px for ≥641px viewports). */
.settings-table {
width: 100%;
border-collapse: collapse;
min-width: 560px;
font-size: 0.93rem;
}
.settings-table th, .settings-table td {
text-align: left;
vertical-align: top;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
}
.settings-table td { white-space: nowrap; }
/* Phase 171 (task 02; TODO L4, owner-locked A3): the group band.
Today's defect: background --surface — the SAME fill as the
.table-wrap card, so the band is invisible. The fix: the
--brand-soft tint (the SOLID hex the sticky thead rule
documents at .table-wrap — rows never show through, and a
themed page's #bor-theme override of the token handles the
color — B4; no new palette variable, B3), weight 700, and a
--line top hairline so a band is delimited above AND below
(the border-bottom comes from the .settings-table th family)
and can never merge with the card's top edge or the band
above it. The text stays --ink on --brand-soft — 14.8:1 (AA;
the :root documented pairing). Static, text-only, no animation
(nothing for prefers-reduced-motion to still). */
.settings-group-row th {
background: var(--brand-soft);
color: var(--ink);
font-weight: 700;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
border-top: 1px solid var(--line);
}
.settings-table tbody tr.settings-row:hover td { background: var(--bg); }
/* The Setting cell: the label (bold) over the BOR_* name (mono,
ink-soft 8.2:1 on --surface, AA). */
.settings-name-cell label { margin: 0; font-weight: 600; min-width: 0; }
.settings-env {
font-family: var(--mono);
font-size: 0.78rem;
color: var(--ink-soft);
}
/* The restart-required flag (LOCKED A4, boot-wired): the accent
STATE family (--accent-ink on --accent-bg 9.5:1, AA) — text +
color, never color alone (B5); the title / aria-describedby
carry the when-it-applies copy. */
.settings-flag {
display: inline-block;
margin-top: 0.25rem;
padding: 0.06rem 0.5rem;
border: 1px solid var(--accent-line);
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
background: var(--accent-bg);
color: var(--accent-ink);
}
/* The Value cell: the type-correct control (number / select /
textarea / text / password) — the .token-label input language
(--line hairline, --surface fill, ink text 15.3:1, AA; the
values are data — mono) + the ≥44px target + the global
:focus-visible ring. */
.settings-control {
width: 100%;
min-width: 0;
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font-family: var(--mono);
font-size: 0.85rem;
}
.settings-control:disabled { opacity: 0.6; }
/* The Source cell: the literal-text badge — the three is-*
variants are SOLID existing tokens (B3): DB is brand-ink on
brand-soft (9.0:1, AA), env is ink-soft on surface (8.2:1, AA;
the --line border keeps it visible on the card), default is
ink-soft on line (7.1:1, AA). The badge is TEXT — the color
is decoration (B5). */
.settings-src {
display: inline-block;
padding: 0.12rem 0.55rem;
border-radius: 999px;
font-family: var(--mono);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
}
.settings-src.is-db { background: var(--brand-soft); color: var(--brand-ink); }
.settings-src.is-env { background: var(--surface); color: var(--ink-soft); border: 1px solid var(--line); }
.settings-src.is-default { background: var(--line); color: var(--ink-soft); }
/* A dirty row: the --brand left marker on the row's first cell
(an inset box-shadow — no layout shift) so "unsaved" survives
grayscale + the N-unsaved-changes hint (text, B5). */
.settings-row.is-dirty > td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
/* The per-row Reset: the .token-revoke language (≥44px, --line
hairline, transparent fill, ink-soft, the err hover pair — it
hands control back to the environment) + the dimmed :disabled
(nothing to reset / read-only). */
.settings-reset {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.settings-reset:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.settings-reset:disabled { opacity: 0.5; cursor: default; }
/* The search row: the .token-create flex (wraps below 640px via
the media query below) + the #token-label input language. */
.settings-search {
display: flex;
align-items: center;
gap: 0.6rem;
}
.settings-search label { white-space: nowrap; }
#settings-filter {
flex: 1;
min-width: 0;
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font: inherit;
font-size: 0.93rem;
}
#settings-filter:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* The action row: Save (the .token-generate brand pill — the
§7.4 in-flight state is the dimmed :disabled + aria-busy; the
disabled cursor is `default` — the .settings-reset:disabled
idiom — the wait cursor was never part of §7.4) and
Clear all (the .token-revoke ghost — the destructive err hover
pair; the two-step confirm reuses the .history-confirm-* rules
unchanged). */
.settings-actions {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
#settings-save {
min-height: 44px;
padding: 0.4rem 1.1rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
#settings-save:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); }
#settings-save:disabled { opacity: 0.6; cursor: default; }
#settings-clear-all {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
#settings-clear-all:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
/* The "N unsaved changes" hint (textContent only, aria-hidden):
the .tokens-status voice (mono, ink-soft). */
.settings-dirty {
font-family: var(--mono);
font-size: 0.8rem;
color: var(--brand-ink);
}
/* The ONE status region (the .tokens-status mirror — the
min-height holds the layout so a line never reflows the table).
The error line (the .theme-error mirror). */
.settings-status {
display: block;
min-height: 1.2em;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
}
.settings-error {
display: block;
margin: 0;
min-height: 1.2em;
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
color: var(--err-ink);
}
/* The no-match row (the .tokens-empty-row language). */
.settings-no-match td {
padding: 0.9rem 1rem;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.85rem;
white-space: normal;
}
/* ---------- Shared page (phase 51, task 03) ----------
/shared/: the anonymous read-only conversation (owner-locked
2026-08-29, TODO.md L6). The shared page reads exactly like the
chat page — both at the 72rem container width (--chat-column, now
equal to the .container cap — owner instruction 2026-09-12; the
wide-desktop doubling was retired) — and the .msg/.bubble/.thinking/
.tool-calls/.msg-meta rules apply unchanged, with NO composer, so
the column contract holds for a guest. Zero interactive controls
(owner-locked): the chips are
plain text, so the pill families' pointer treatments are switched
off IN THIS SCOPE ONLY — the chat page's interactive chips keep
their styles untouched. Every pair reuses the Phase-08 AA palette
(ink-soft ≥6.9:1 on surface/bg, brand-ink on brand-soft 6.9:1);
:focus-visible via the global 3px outline rule. No CDN, system
fonts. */
.shared-shell {
width: 100%;
max-width: var(--chat-column); /* the centered chat column — 72rem,
the .container width at every viewport */
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
}
/* Page title (JS-filled with the shared chat's title; the static
fallback is "Shared conversation") — the page-head h1 size. */
#shared-title { margin: 0 0 0.25rem; font-size: 1.7rem; }
/* The muted meta line under the h1 ("Shared via … — read-only."):
ink-soft on the page bg ≥8.6:1, the page-sub language. */
.shared-note {
margin: 0;
color: var(--ink-soft);
font-size: 0.92rem;
}
/* The invalid / revoked state: a centered muted card (the not-found
language — surface fill, --line border, italic ink-soft). Revealed
by shared.js for a malformed token (no fetch) or a failed read. */
#shared-invalid {
padding: 1.4rem 1rem;
text-align: center;
color: var(--ink-soft);
font-style: italic;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
/* Static chips: a guest's "Maybe try" and source chips are TEXT — no
pointer, no hover, no cursor (owner-locked zero controls). The pill
look is kept; the interactivity is scoped off here and only here
(pointer-events: none also makes the :hover rules unreachable). */
.shared-shell .suggestion-chip,
.shared-shell .source-chip {
pointer-events: none;
cursor: default;
}
/* ---------- Document viewer (phase 10; two-row header since phase 34) ---------- */
/* Phase 34 (owner confirmation 2026-08-26): the viewer header is TWO
rows in one sticky — row 1 reuses the standard .app-header /
.header-inner rules VERBATIM (row 1 IS the standard bar, so the
phase-12/19 pinned heights 64px / 58px apply), row 2 is the
.doc-titlebar (back link + title + meta). The header element itself
is content-sized (row 1 + row 2) and sticky, so BOTH rows stay
pinned while the document scrolls. */
.doc-header {
position: sticky;
top: 0;
z-index: 20;
background: var(--surface);
/* Phase 34: no fixed height — the header sizes to row 1 (--header-h)
+ the .doc-titlebar; the phase-12 shrink guard stays. */
flex-shrink: 0;
}
/* Row 1's own gradient hairline would land BETWEEN the rows; the
separator there is the quiet --line border-top on the titlebar, and
the signature hairline belongs at the bottom edge of the whole
header (the .doc-header::after in the shared rule above). */
.doc-header > .app-header::after { content: none; }
/* Back link: pill with an SVG arrow + "Sources" (>=44px touch target;
:focus-visible via the global 3px outline rule, phase 08). */
.doc-back {
display: inline-flex;
align-items: center;
gap: 0.4rem;
flex: 0 0 auto;
min-height: 44px;
padding: 0.45rem 0.9rem;
border-radius: 999px;
background: var(--brand-soft);
border: 1px solid var(--line);
color: var(--brand-ink); /* 6.9:1 on --brand-soft */
font-weight: 600;
font-size: 0.95rem;
text-decoration: none;
}
.doc-back:hover { background: var(--brand-soft); }
.doc-back svg { width: 16px; height: 16px; display: block; }
/* Row 2: the titlebar — a .container-width row with the back link +
the title block, its own content-sized height (title line + meta
line), the same surface as row 1, separated from it by the quiet
hairline (var(--line) — the existing 1px border color). */
.doc-titlebar {
border-top: 1px solid var(--line);
padding-block: 0.6rem;
}
.doc-titlebar .container {
display: flex;
align-items: center;
gap: 0.9rem;
}
/* The title block keeps clipping (min-width: 0) so #doc-title /
.doc-meta ellipsize inside the flex row instead of overflowing. */
.doc-title-block { min-width: 0; }
#doc-title {
margin: 0;
font-size: 1.3rem;
line-height: 1.3;
/* Phase 34: ellipsis + the `title` attribute (set by document.js) —
the row has no pills to fit, so the old clipping-for-pills rule is
gone; the ellipsis is the only fit the title needs. */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Meta row: source badge · format badge · mono path · created ·
indexed · chunks (phase 106 (task 08, D8) added created BEFORE
indexed). Phase 12/34: it may clip, but it must NEVER wrap — the
meta stays on one line in the titlebar row (the row is content-
sized, so a wrap would grow it). */
.doc-meta {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.4rem;
margin-top: 0.35rem;
font-size: 0.78rem;
color: var(--ink-soft);
/* Overflowing content (badges + path + dates) clips at the bar edge;
nowrap keeps text on one line so the row can never grow the header. */
white-space: nowrap;
overflow: hidden;
}
.doc-source-badge {
background: var(--brand-soft);
border: 1px solid var(--line);
color: var(--brand-ink);
font-weight: 600;
border-radius: 999px;
padding: 0.02rem 0.55rem;
}
.format-badge {
font-family: var(--mono);
background: var(--brand-soft);
border: 1px solid var(--line);
color: var(--brand-ink);
border-radius: var(--radius-sm);
padding: 0.02rem 0.45rem;
}
.doc-path {
font-family: var(--mono);
font-size: 0.75rem;
max-width: 26rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* The path is the meta row's designated ellipsis target: with a floor it
keeps a visible box on narrow screens while the trailing badges clip. */
min-width: 6rem;
}
/* Phase 145 (task 03): the "Original" anchor — the git origin as a
copyable external link in the meta row (rendered by the ONE
renderDocument core — the modal AND /document.html; a non-git
document never gets one). The .doc-path code family (mono, 0.75rem,
the SAME 26rem ellipsis budget + 6rem floor — the row clips, the
title carries the full URL) with the .doc-link affordance (brand-ink
8.7:1 on --surface — the recorded .doc-link pair; underline on
hover/focus, the .doc-link idiom). The anchor's visible text IS the
full URL (the copyable-link contract — the ellipsis clips the
display). */
.doc-origin {
font-family: var(--mono);
font-size: 0.75rem;
max-width: 26rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 6rem;
color: var(--brand-ink); /* 8.7:1 on --surface (the .doc-link pair) */
text-decoration: none;
}
.doc-origin:hover, .doc-origin:focus-visible { text-decoration: underline; }
/* Phase 153 (task 02): the meta row's "Copy path" control — the
row-action language (icon + label, the .git-source-remove idiom)
in a NEUTRAL secondary fill (the .git-source-ignore hover pair —
brand-soft, 12.4:1 on --surface; the meta row's text sits on the
titlebar's --surface). Compact on desktop (the meta row's 0.78rem
scale — a 44px floor would grow the bar); the ≤640px block gives
it the ≥44px touch-target floor (PLAN §7.1). The VISIBLE "Copy
path" text is the accessible name (no aria-label); the icon is
aria-hidden. */
.doc-copy-path {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.12rem 0.5rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (the meta row's pair) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
white-space: nowrap;
cursor: pointer;
}
.doc-copy-path svg { width: 13px; height: 13px; display: block; flex: 0 0 auto; }
.doc-copy-path:hover { background: var(--brand-soft); color: var(--brand-ink); }
.doc-copy-path:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Phase 153 (task 02): the Copy-path status line — the VISIBLE
confirmation (aria-hidden — the surface's role=status live region
is the a11y announcer, the share pattern): the meta row's text
scale, var(--ink-soft) on --surface (5.1:1 — the pair recorded at
:root). Serves BOTH surfaces: the page's floating block and the
modal's wrapping meta block. */
.doc-copy-status {
margin: 0;
font-size: 0.78rem;
color: var(--ink-soft); /* 5.1:1 on --surface */
}
/* Phase 153 (task 02): the /document.html page's Copy-path state
block — a floating panel anchored under the control (absolutely
positioned in the header — the meta row must never wrap or grow
the bar, the phase-12/34 contract, so the state floats OUT of
the row). Surface fill, --line border, the standard shadow;
wraps (the field + the status may share a row or take separate
rows at narrow widths). z-index 30 sits inside the header's own
z-20 root context, so the block floats above the page content. */
.doc-copy-pop {
position: absolute;
z-index: 30;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
max-width: min(36rem, calc(100vw - 2rem));
padding: 0.45rem 0.6rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
box-shadow: var(--shadow);
}
/* Phase 153 (task 02): the Copy-path inline fallback field — the
.share-link-fallback pattern (input variant): a non-secure (http)
origin rejects the clipboard, so the full path is offered as an
input-like that selects itself on focus — click or Tab,
then Ctrl/Cmd+C. readOnly (a copy surface, not an editor), mono
(the path is data — the .share-link-fallback font), surface
fill, --line border; truncates with an ellipsis at narrow widths
(the selection is the copy). Ink on surface ≈13.8:1 (the
.share-link-fallback pair). */
.doc-path-fallback {
max-width: 26rem;
padding: 0.3rem 0.55rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font-family: var(--mono);
font-size: 0.78rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.doc-path-fallback:hover { border-color: var(--brand); }
.doc-path-fallback:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
.doc-chunks { font-family: var(--mono); }
/* Phase 106 (task 08, D8): the Created badge — the SAME family as the
Indexed one (no own look: the meta row's text, var(--ink-soft) on
the titlebar's --surface — 5.1:1, the pair recorded at :root; the
modal's .doc-modal-meta row records the same pair). The explicit
declaration pins the inherited value: a themed --ink-soft moves the
badge with the row (byte-identical rendering). */
.doc-created { color: var(--ink-soft); /* 5.1:1 on --surface */ }
.doc-shell {
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
}
#doc-content { display: flex; flex-direction: column; }
.doc-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); }
/* Markdown: the centered reading column — 72rem, the .container width
at every viewport (owner instruction 2026-09-12: "match the width
of the RAG page for all other pages" — the wide-desktop doubling
was retired). */
.doc-md {
width: 100%;
max-width: var(--chat-column);
margin-inline: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.5rem 1.75rem;
overflow-wrap: anywhere;
}
.doc-md p { margin: 0.35rem 0; }
.doc-md h3 { margin: 1.1rem 0 0.4rem; font-size: 1.15rem; }
.doc-md h4 { margin: 0.9rem 0 0.35rem; font-size: 1rem; }
.doc-md > :first-child { margin-top: 0; }
.doc-md ul { margin: 0.4rem 0; padding-left: 1.3rem; }
.doc-md pre {
background: var(--surface);
color: var(--ink);
padding: 0.7rem 0.9rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
overflow-x: auto;
font-size: 0.85rem;
font-family: var(--mono);
}
.doc-md code { font-family: var(--mono); font-size: 0.88em; background: var(--brand-soft); padding: 0.08em 0.35em; border-radius: 5px; }
.doc-md pre code { background: none; padding: 0; }
/* Summary panel (phase 36): the labeled "Summary" section the shared
renderDocument core (document.js) draws ABOVE the original content
whenever doc.summary is non-empty (phase 30 — non-markdown docs
only). A "summary, not content" look: the surface card carries a 3px
brand left border (no shadow — the content cards own those) and a
small-caps brand-ink label. Phase-08 tokens only; static content —
no animation (nothing for prefers-reduced-motion to still), and the
aria-label + heading carry the accessibility. */
.doc-summary {
width: 100%;
background: var(--surface);
border: 1px solid var(--line);
border-left: 3px solid var(--brand);
border-radius: var(--radius);
padding: 0.9rem 1.1rem;
margin-bottom: 1rem;
overflow-wrap: anywhere;
}
/* md/markdown: the panel matches the .doc-md centered reading column
(72rem — the same --chat-column token, the .container width at
every viewport) — it is the column's label. Raw formats stay full width (the
.doc-raw default above), matching the full-width pre; in engines
without :has() the panel degrades to that full-width default. */
.doc-summary:has(+ .doc-md) {
max-width: var(--chat-column);
margin-inline: auto;
}
.doc-summary-title {
margin: 0 0 0.4rem;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brand-ink); /* 9.0:1 on --surface */
}
.doc-summary-text {
margin: 0;
color: var(--ink); /* on --surface ≈14.5:1 */
}
/* Summary edit affordance (phase 57, task 02 — D4, admin-only): the
header row (label + Edit button), the inline editor (prefilled
textarea + Save/Cancel + a role=status live region). House
dark-tech palette (phase-08 tokens), system fonts, no CDN;
:focus-visible via the global 3px outline rule. Anonymous visitors
never see any of it — the button and editor are wired only for
admins (docAdminReady in document.js), so the public panel is
byte-for-byte the phase-36 shape. */
.doc-summary-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.4rem; /* the old .doc-summary-title bottom margin */
}
.doc-summary-head .doc-summary-title { margin: 0; }
.doc-summary-edit {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
letter-spacing: 0.02em;
cursor: pointer;
}
.doc-summary-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
.doc-summary-edit[hidden] { display: none; } /* the hidden attr must beat the display above */
.doc-summary-editor {
display: block;
width: 100%;
min-height: 8rem;
padding: 0.6rem 0.8rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg); /* inset against the --surface panel */
color: var(--ink); /* 16.7:1 on --bg (AA) */
font: inherit;
line-height: 1.5;
resize: vertical;
}
.doc-summary-actions {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.75rem;
}
.doc-summary-save {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0.35rem 0.95rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */
font: inherit;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
}
.doc-summary-save:hover { background: var(--brand-hover); } /* the house hover lightening */
.doc-summary-save:disabled { opacity: 0.6; cursor: default; } /* one PATCH at a time */
.doc-summary-cancel {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0.35rem 0.95rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
}
.doc-summary-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.doc-summary-status {
margin: 0.6rem 0 0;
font-size: 0.85rem;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
}
.doc-summary-status:empty { margin-top: 0; }
/* Date edit affordance (phase 106, task 09 — D7, admin-only): the
"Edit date" text button after the Created badge (the
.doc-summary-edit family — one pill style for both inline
editors) and the editor box that swaps in for the button IN the
.doc-meta row: the native type=date input, Save / Cancel, the
muted underlined "Revert to sync" clear affordance (the phase-57
"clear = explicit" contrast), a role=status live line, and a
role=alert error line (the .git-source-error err pair). The meta
row may clip (nowrap) on the page, so the box wraps ITS children
internally (the modal's .doc-modal-meta already wraps). The
global 3px :focus-visible outline rule applies — no per-control
rule (the phase-105 checkbox idiom). :disabled = opacity +
cursor: wait (the .git-source-remove:disabled idiom — one PATCH
at a time). Contrast (verified, house style): button / Cancel /
revert / status ride the row's ink-soft on --surface (5.1:1, AA);
the input is ink on --bg (16.7:1, AA); Save is the solid brand
pill (--bg on --brand = 5.2:1, AA); the error line is the err
pair (9.1:1 on --err-bg, AA). Anonymous / token holders never see
any of it — the wiring is admin-gated (docAdminReady in
document.js), so the public badge row stays byte-for-byte the
task-08 shape. */
.doc-date-edit {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
letter-spacing: 0.02em;
cursor: pointer;
}
.doc-date-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
.doc-date-edit[hidden] { display: none; } /* the hidden attr must beat the display above */
.doc-date-editor {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
min-width: 0;
max-width: 100%;
}
.doc-date-input {
padding: 0.15rem 0.4rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg);
color: var(--ink); /* 16.7:1 on --bg (AA) */
font: inherit;
font-size: 0.78rem;
font-family: var(--mono);
}
.doc-date-save {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
cursor: pointer;
}
.doc-date-save:hover { background: var(--brand-hover); } /* the house hover lightening */
.doc-date-cancel {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
cursor: pointer;
}
.doc-date-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.doc-date-revert {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.2rem;
border: 0;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) — the muted marker */
font: inherit;
font-size: 0.78rem;
text-decoration: underline;
cursor: pointer;
}
.doc-date-revert:hover { color: var(--ink); } /* 14.5:1 on --surface (AA) */
.doc-date-save:disabled,
.doc-date-cancel:disabled,
.doc-date-revert:disabled,
.doc-date-input:disabled {
opacity: 0.5;
cursor: wait; /* one PATCH at a time (the .git-source-remove:disabled idiom) */
}
.doc-date-status {
font-size: 0.78rem;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
}
.doc-date-error {
flex-basis: 100%; /* drops onto its own row under the controls */
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere; /* a long server detail never overflows the box */
font-size: 0.78rem;
font-weight: 600;
background: var(--err-bg);
color: var(--err-ink); /* 9.1:1 on --err-bg (AA — the .git-source-error pair) */
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.1rem 0.5rem;
}
.doc-date-status:empty, .doc-date-error:empty { display: none; } /* an empty live line takes no space */
/* Raw (non-markdown, non-prose) formats: full-width mono pre,
horizontal scroll (locked A3, phase 165 — the prose set renders in
the wrapping .doc-prose block below, never here). */
.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;
}
/* Prose (phase 165, task 01 — TODO.md L3, LOCKED A1/A2): the image
description and plain-text (txt/text) docs as WRAPPING prose — the
.doc-md column geometry (72rem reading column, the surface card)
with pre-wrap rhythm. The defect: these docs landed in the .doc-raw
pre above (white-space: pre), so every generated line was one
unbreakable line and a long sentence scrolled sideways. The content
stays a text node (pre-wrap carries the paragraph breaks — no
markdown parsing: a description is prose, not markup). Tokens only
(no new hue — the monochrome invariant; --ink on --surface is the
documented 14.5:1 AA pair). Static content — no animation (nothing
for prefers-reduced-motion to still). Code keeps the .doc-raw
scroller (phase 116 locked A1, here locked A3). */
.doc-prose {
width: 100%;
max-width: var(--chat-column);
margin-inline: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.5rem 1.75rem;
color: var(--ink);
white-space: pre-wrap;
overflow-wrap: anywhere;
line-height: 1.55;
}
/* Image documents (phase 122, task 04): the viewer's image block —
the document's persistent bytes (the /api/documents/{id}/image
route) above its description (the .doc-raw content slot below). The
theme's surface card treatment (the .doc-md language): the image
sits on --surface with the line border + radius; max-width 100% +
max-height 70vh keep any aspect ratio inside the page (height: auto
keeps the ratio). Static content — no animation; the alt text (the
vision description) carries the accessibility (WCAG). Phase-08
tokens only (no new hue — the monochrome invariant). */
.doc-image {
width: 100%;
max-width: var(--chat-column);
margin: 0 auto 1rem;
display: flex;
justify-content: center;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 0.5rem;
}
.doc-image-img {
display: block;
max-width: 100%;
max-height: 70vh;
height: auto;
border-radius: var(--radius-sm);
background: var(--bg);
}
/* The vector variant (phase 181, task 04, design D6 — applied in
document.js when doc.format === "svg", base class kept). An SVG
carrying only a viewBox has NO intrinsic size, so without an
explicit width the default replaced-element box (300×150) applies
and the drawing sits small inside the full-width .doc-image card;
width: 100% spans the card (the full-size preview the owner
asked for). object-fit: contain protects the drawing from being
squashed when the base rule's max-height: 70vh clamps a tall
vector — the box letterboxes, the graphic keeps its aspect ratio.
Static content — no animation (nothing for prefers-reduced-motion
to still). Palette-neutral: no colour here — the base rule's --bg
shows through (phase-08 tokens only, no new hue). */
.doc-image-img--vector {
width: 100%;
height: auto;
object-fit: contain;
}
/* The onerror fallback (the route's 404 — the row exists but the
copy was lost): a muted note in the card's place-of-image; the
description below still renders, so the doc stays readable. Phase
148 (task 03) reuses THIS class for the page strip's lost-page
note (the same muted styling — no new pattern). */
.doc-image-unavailable {
margin: 1.25rem 1rem;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font-size: 0.9rem;
}
/* Page strip (phase 148, task 03): the viewer's rendered-page strip
for a page-qualifying PDF — the payload's page_urls (task 02's
exact 1-based route list) replaces the text content AND the
.doc-summary panel (the owner's contract: the per-page
descriptions stay in the DB for Q&A, just not shown to the user).
The core renders it into the SAME #doc-content slot the .doc-raw /
.doc-md paths use — no new column — inside the existing doc
content column: --chat-column (72rem — the .container width, the
.doc-md / .doc-image language) at every viewport. A vertical stack
(one page per row — the natural reading order for instruction
manuals); each figure carries the page PNG on the non-clashing
--surface with the line border + radius (the .doc-image figure
precedent — a page PNG must sit on a surface in both themes) and a
centered muted "Page N" caption. Static content — no animation
(nothing for prefers-reduced-motion to still); the lazy imgs + alt
text (WCAG) carry the accessibility. Phase-08 tokens only (no new
hue — the monochrome invariant). */
.doc-pages {
display: grid;
gap: 1rem;
width: 100%;
max-width: var(--chat-column);
margin-inline: auto;
}
.doc-page { margin: 0; }
.doc-page img {
display: block;
width: 100%;
max-width: 100%;
height: auto;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.doc-page-num {
font-size: 0.78rem;
color: var(--ink-soft); /* 8.2:1 on --surface, ~9.7:1 on --bg (AA in both themes) */
text-align: center;
padding: 0.35rem 0 0;
}
/* Additive strip view (phase 163, task 04): the pictorial-class PDF
— a USABLE text layer — keeps the text doc (Summary panel +
content) with the page strip one Text/Pages toggle away (LOCKED
A1/A6). The toggle is the house ghost-pill idiom (the .auth-link
family): the two pills sit adjacent in the labeled group, ink-soft
on the transparent pill (AA on --bg/--surface), hover the
brand-soft/brand-ink pair; the PRESSED button is the brand fill +
heavier weight — text + style, never color alone (B5) — with --bg
ink on --brand (5.2:1, the .share-chat-btn pair). ≥44px targets
(§7.1); :focus-visible rides the global 3px ring. The view swap is
CSS-driven off the .doc-views wrapper's data-view: the strip is
display:none by default (its lazy imgs never fetch while hidden)
and shows on [data-view="pages"], which hides the text view instead
— the nodes stay mounted across flips (no re-render, no refetch).
Phase-08 tokens only (no new hue — the monochrome invariant). */
.doc-view-toggle {
display: flex;
gap: 0.4rem;
width: 100%;
max-width: var(--chat-column);
margin-inline: auto;
margin-bottom: 0.75rem;
}
.doc-view-btn {
display: inline-flex;
align-items: center;
justify-content: center;
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;
}
.doc-view-btn:hover { background: var(--brand-soft); color: var(--brand-ink); }
.doc-view-btn[aria-pressed="true"] {
background: var(--brand);
border-color: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 (AA) */
font-weight: 700; /* pressed = weight + fill, never color alone (B5) */
}
.doc-views .doc-pages { display: none; } /* Text default — hidden lazy imgs never fetch */
/* The text views hide behind the strip on Pages. .doc-prose (phase
165, task 01) joins the list DEFENSIVELY today — an image/txt doc
never page-qualifies — and becomes load-bearing the day the owner
adds "pdf" to PROSE_FORMATS (phase 165 D1 + phase 163 A2). */
.doc-views[data-view="pages"] .doc-summary,
.doc-views[data-view="pages"] .doc-md,
.doc-views[data-view="pages"] .doc-raw,
.doc-views[data-view="pages"] .doc-prose,
.doc-views[data-view="pages"] .doc-image { display: none; }
.doc-views[data-view="pages"] .doc-pages { display: grid; }
/* Designed not-found state (no emoji — plain SVG mark, phase 08 rule). */
.doc-not-found {
width: 100%;
max-width: 30rem;
margin: 2rem auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2.25rem 1.75rem;
text-align: center;
}
.doc-not-found-glyph { color: var(--ink-soft); width: 44px; height: 44px; margin-inline: auto; }
.doc-not-found-glyph svg { width: 44px; height: 44px; display: block; }
.doc-not-found h2 { margin: 0.8rem 0 0.4rem; font-size: 1.3rem; }
.doc-not-found-sub { margin: 0 0 1.25rem; color: var(--ink-soft); }
.doc-open-sources {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0.5rem 1.1rem;
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 */
font-weight: 700;
text-decoration: none;
border-radius: var(--radius-sm);
}
.doc-open-sources:hover { background: var(--brand-hover); }
/* Viewer links: Sources-table path cell + chat source chips (phase 10). */
.doc-link {
color: var(--brand-ink); /* 8.7:1 on --surface */
text-decoration: none;
}
.doc-link:hover, .doc-link:focus-visible { text-decoration: underline; }
/* ---------- Document modal (phase 26) ----------
"New documents should open in an almost-fullscreen modal, not in a new
page" (TODO.md L4). The overlay reuses the viewer page's .doc-meta
badge classes, the .doc-md reading column (72rem — the .container
width; inside the modal the 1100px panel stays its effective
ceiling), and the .doc-raw
pre — this block only adds the chrome (backdrop, panel, header,
actions, scroll container). Phase-08 tokens only; NO blur (the
phase-08 no-blur perf anchor); no new assets; system fonts.
Stacking: z-index 1000 puts the overlay above the sticky header (20)
and the skip-link (100); the z-index:-1 background layers stay below
everything. The panel is 96vw × 92vh, centered ("almost-fullscreen"). */
.doc-modal {
position: fixed;
inset: 0;
z-index: 1000;
display: flex; /* the panel is the only in-flow child — margin: auto centers it */
}
/* Explicit (the global [hidden] rule already wins — this one is the
documented, testable contract for the skeleton). */
.doc-modal[hidden] { display: none; }
.doc-modal-backdrop {
position: fixed;
inset: 0;
/* --bg at 82% — no backdrop-filter (phase-08 no-blur perf anchor). */
background: color-mix(in srgb, var(--bg) 82%, transparent);
transition: opacity 120ms ease;
}
.doc-modal-panel {
/* position:relative lifts the panel above the fixed backdrop (positioned
elements paint over in-flow siblings otherwise). */
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(1100px, 96vw);
height: 92vh;
margin: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 12px;
/* Depth shadow — deliberately NOT themed (black, phase-92 table:
"depth, not theme"); the color-mix spelling renders the same 55%
black and keeps the literal out of the phase-92 sweep gate. */
box-shadow: 0 24px 80px color-mix(in srgb, black 55%, transparent);
}
/* Sticky top with the SAME height as the page bars — the phase-12 pins
(--header-h: 64px desktop / 58px ≤640px) so the bar never reads
differently here. The title is the designated squeeze target (ellipsis),
so the bar can never grow its height. */
.doc-modal-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.9rem;
height: var(--header-h);
padding-inline: 1.25rem;
background: var(--surface);
border-bottom: 1px solid var(--line);
}
.doc-modal-title {
min-width: 0;
margin: 0;
font-size: 1.3rem;
line-height: 1.3;
color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.doc-modal-actions {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* "Full page" escape hatch: the same ghost pill as New chat / Sign in
(ink-soft on surface ≈6.9:1; hover pair brand-ink/brand-soft ≈6.9:1).
Icon-only below 640px — the aria-label keeps the accessible name. */
.doc-modal-open {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
padding: 0.4rem 0.8rem;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.85rem;
white-space: nowrap;
text-decoration: none;
}
.doc-modal-open:hover { background: var(--brand-soft); color: var(--brand-ink); }
.doc-modal-open svg { width: 15px; height: 15px; display: none; }
/* Icon-only close (aria-label in the markup). ink-soft on surface ≈6.9:1;
hover = the err pair ≈9.1:1, like the steering-note delete. */
.doc-modal-close {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
padding: 0;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
cursor: pointer;
}
.doc-modal-close:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.doc-modal-close:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 2px;
}
.doc-modal-close svg { width: 18px; height: 18px; display: block; }
/* Meta row: the SAME badge classes as the viewer's .doc-meta (source
badge · format badge · mono path · created · indexed · chunks — no
duplicate badge styling here); unlike the fixed-height header bar it
may WRAP, so nothing clips. aria-live="polite" on the element
announces the load → meta swap (phase-10 a11y contract, modal
variant). */
.doc-modal-meta {
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
padding: 0.6rem 1.25rem 0;
font-size: 0.78rem;
color: var(--ink-soft);
}
/* The scroll container: vertical scroll lives HERE, never the viewport.
.doc-md keeps its centered reading column inside (72rem — wider
than the 1100px panel's inner width, which stays its effective
ceiling); .doc-raw keeps its own overflow-x. tabindex="-1" in the
markup is the JS focus target. */
.doc-modal-content {
flex: 1;
min-height: 0;
overflow: auto;
padding: 1rem 1.25rem 1.5rem;
}
/* Themed horizontal scrollbar for the modal's raw code block (phase 116,
task 01): the .doc-raw pre already owns its overflow (its own
`overflow-x: auto` + `white-space: pre` — locked A1, code never
wraps), so the native light-gray bar a long line reveals is the pre's
own. Both engine families are themed, scoped to THIS modal only (no
global scrollbar restyle — the TODO's scope): scrollbar-color / -width
for Firefox, the ::-webkit-scrollbar pseudos for Chromium (height 8px
= the horizontal bar; the pre does not scroll vertically). The tokens
follow the saved theme via the :root color-mix derivation; the muted
thumb keeps >= 3:1 non-text contrast on the near-surface track. */
.doc-modal .doc-raw {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.doc-modal .doc-raw::-webkit-scrollbar { height: 8px; }
.doc-modal .doc-raw::-webkit-scrollbar-track { background: var(--scrollbar-track); }
.doc-modal .doc-raw::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 4px;
}
.doc-modal-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); }
/* No motion under reduced motion (same pattern as the phase-25 layers). */
@media (prefers-reduced-motion: reduce) {
.doc-modal-backdrop { transition: none; }
}
/* ---------- Doc edit screen (phase 59, task 06) ----------
/doc-edit.html: the admin-gated edit screen for a doc draft (title,
in-repo path, markdown body) — a FLOW page, not one of the app's
pages, so the header is SLIM (brand + "← Back to chat" only). The
shell rides the full 72rem container like every other page (owner
instruction 2026-09-12 — the phase-59 form-column cap is
superseded). The .sources-gate gate is reused verbatim
(phases 16/35/50). Every pair reuses the Phase-08 AA palette;
touch targets >=44px; :focus-visible via the global 3px outline
rule. No CDN, system fonts. */
.doc-edit-shell {
width: 100%;
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* The slim header's back link — the .doc-back ghost language
(document.html): >=44px target, --line border, ink-soft (5.1:1 on
the --surface bar) rising to ink on hover; pushed to the bar's right
edge by the header-inner flex (margin-left: auto — the nav's
own pattern). */
.doc-edit-back {
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-height: 44px;
margin-left: auto;
padding: 0.4rem 0.9rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font-weight: 600;
text-decoration: none;
}
.doc-edit-back:hover { color: var(--ink); border-color: var(--ink-soft); }
.doc-edit-back svg { width: 16px; height: 16px; display: block; }
/* The edit form — the tuning form's card as a vertical stack: labeled
title input, mono path input, the mono markdown textarea (min-height
20rem — the body is the star), and the actions row (the brand Push
button + the back link). Inset fields (bg fill on the surface card). */
#doc-edit-form {
display: flex;
flex-direction: column;
gap: 0.9rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.5rem 1.5rem 1.75rem;
}
#doc-edit-form label {
font-weight: 600;
font-size: 0.95rem;
}
#draft-title,
#draft-path {
width: 100%;
font: inherit;
font-size: 1rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.55rem 0.75rem;
min-height: 44px;
}
/* The in-repo path is machine data — mono (the git-sources URL-input
convention). */
#draft-path {
font-family: var(--mono);
font-size: 0.92rem;
}
/* The markdown body: mono, tall (min-height 20rem), vertical resize.
ink on bg = 16.7:1. */
#draft-body {
width: 100%;
font-family: var(--mono);
font-size: 0.92rem;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.75rem 0.9rem;
min-height: 20rem;
resize: vertical;
}
/* Actions row: the primary Push button (brand, dark ink on brand
5.2:1 — never white on brand) + the Discard control (phase 115 —
the secondary destructive action) + the back link; wraps at narrow
widths. */
.doc-edit-actions {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
#push-doc-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
border: 0;
border-radius: var(--radius-sm);
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 */
font: inherit;
font-weight: 700;
cursor: pointer;
padding-inline: 1.25rem;
}
#push-doc-btn:hover:not(:disabled) { background: var(--brand-hover); }
#push-doc-btn:disabled { opacity: 0.6; cursor: wait; }
/* Phase 115 (task 02): the Discard control — the SECONDARY destructive
action, visually subordinate to the brand primary (the ghost family
of .steering-delete / .tuning-delete): ink-soft on transparent
(5.1:1 on --surface — more on the darker --bg canvas) with the
--line border; hover joins the err family (err-ink on err-bg 9.3:1,
the err-line border — the state stays text + color, never color
alone); the 44px touch floor, the global 3px :focus-visible ring,
and the :disabled state is the "Discarding…" in-flight affordance. */
.discard-draft {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.85rem;
white-space: nowrap;
padding-inline: 1rem;
cursor: pointer;
}
.discard-draft:hover:not(:disabled) {
background: var(--err-bg);
color: var(--err-ink);
border-color: var(--err-line);
}
.discard-draft:disabled { opacity: 0.6; cursor: wait; }
/* The success status line (role=status): the ok family (ok-ink on
ok-bg 10.6:1) when a push outcome has landed — min-height holds the
line's space so the layout never jumps when the text lands. Empty
(before the first push, or after a failure cleared the stale line)
it is the dashed placeholder (the #archive-upload-result language). */
.doc-edit-status {
margin: 0;
min-height: 1.5rem;
background: var(--ok-bg);
color: var(--ok-ink);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.8rem;
font-size: 0.9rem;
font-weight: 600;
}
.doc-edit-status:empty {
background: transparent;
border-style: dashed;
color: var(--ink-soft);
}
/* The error banner (role=alert): the err family (err-ink on err-bg
9.3:1, err-line border) — git's stderr may carry long paths, so
long words break instead of overflowing the card. */
.doc-edit-error {
margin: 0;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.5rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
overflow-wrap: anywhere;
}
/* ---------- Footer ---------- */
.app-footer {
border-top: 1px solid var(--line);
background: var(--surface);
padding-block: 0.8rem;
margin-top: 1rem;
}
.footer-inner {
display: flex;
justify-content: space-between;
gap: 1rem;
color: var(--ink-soft);
font-size: 0.85rem;
}
/* ---------- Responsive (tablet squeeze — phase 34 task 04 visual pass) ----------
The full admin bar (brand + nav [Chat, Sources, Tuning] + Tuning
toggle + New chat + Sign out) outgrows a 768px bar in
the desktop styles. Squeeze the pills/gaps moderately — the 44px
touch floor is held by min-height and the 64px height by --header-h
— and let the brand wordmark (base ellipsis) absorb any remainder.
The ≤640 block below stays tighter and wins at phone widths. */
@media (max-width: 900px) {
.header-inner { gap: 0.6rem; }
/* Phase 35: the admin-only "Git sources" link joins the nav — a
fourth text pill on the admin bar — so the tablet squeeze tightens
once more to hold 768px in the admin state (brand already the
designated clip target, pills squeeze next). */
.nav-link { padding: 0.4rem 0.5rem; font-size: 0.85rem; }
.app-nav { gap: 0.15rem; }
.new-chat-btn, .auth-link { padding: 0.45rem 0.5rem; }
}
/* ---------- Responsive (mobile-first adjustments) ---------- */
@media (max-width: 640px) {
:root { --header-h: 58px; }
.container { padding-inline: 0.9rem; }
/* Owner report 2026-09-08: the hamburger "won't extend" on a real
phone while the same width in a desktop browser works. The page
opts into `viewport-fit=cover` (the ), so on notched phones
the layout viewport runs UNDER the status bar / Dynamic Island —
and this bar (58px, toggle tap zone y≈7–51px) sits entirely in the
system top-edge zone: visually under the battery/signal icons, and
in the region where a real device does not reliably deliver taps
to the page (desktop emulation has no system top zone, so it works
there — the device-only failure). Pad the bar DOWN by the top
safe-area inset: the header surface still fills the notch region
(that is the point of viewport-fit=cover) but every control — the
#nav-toggle first — lands in the reliably tappable area, and the
dropdown's top:100% follows the taller bar automatically. The
.app-header rule doubles as the document viewer's row-1 bar (the
same class), so the viewer's hamburger is covered by the same
rule. env() resolves to 0 on every non-notch surface (desktop,
headless, no-island phones) — byte-identical rendering there. */
.app-header {
height: calc(var(--header-h) + env(safe-area-inset-top, 0px));
padding-top: env(safe-area-inset-top, 0px);
}
/* The share toast rests just under the bar — the bar is taller by
the same inset on notched phones, so the offset must carry it too
(the toast would otherwise tuck under the padded bar). */
.toast {
top: calc(var(--header-h) + 0.75rem + env(safe-area-inset-top, 0px));
}
/* Phase 14: the New chat pill joins the header — tighten the bar so
brand + nav + pill fit at 360px without horizontal overflow (the
brand text may ellipsize as the designated squeeze target). */
/* Phase 29: the admin-only Tuning link joins the nav — three text
pills on the chat bar in the admin state. The brand text (the
designated squeeze target) is already at zero, so the bar squeezes
the nav pills + gaps instead: tighter pill padding/font and a
tighter inner gap. The 44px touch floor is held by min-height, the
58px bar height is pinned by --header-h, and the bar now fits both
auth states at 375px (and 360px) without horizontal overflow.
Phase 34 task 04 (visual pass): the full admin bar — nav [Chat,
Sources, Tuning] + Tuning toggle + Sync + New chat + Sign out —
now ships on EVERY page, so the squeeze tightens once more
(0.3rem inner gap, 0.78rem nav pills, 0.35rem pill padding,
18px brand mark) to hold 375px with the brand mark intact and
360px with the brand clipped clean (overflow:hidden — the mark
never overlaps the nav). */
/* Phase 35: the admin-only "Git sources" link joins the nav — a
fourth text pill on the admin bar — so the mobile squeeze tightens
once more (0.25rem inner gap, 0.72rem nav pills, 0.3rem pill
padding, 0.05rem nav gap) to hold 375px — and 360px, the brand
fully clipped — in the admin state without horizontal overflow. */
.header-inner { gap: 0.25rem; }
.brand { min-width: 0; overflow: hidden; }
.brand-mark { width: 18px; height: 18px; }
.brand-text {
font-size: 0.8rem;
letter-spacing: 0.04em;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Phase 46 (owner permission 2026-08-27, TODO.md L9): the nav links
LEAVE the bar at phone widths — the old pill-squeeze rules for
.nav-link / .app-nav (0.72rem pills, 0.05rem gap, in place of this
comment) are superseded by the #nav-toggle dropdown below. The
action pills' squeeze rules further down are untouched, and the
900px tablet block keeps squeezing the INLINE nav at 641–900px
(the hamburger is absent there). */
.nav-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
padding: 0;
color: var(--ink);
background: none;
border: 0;
border-radius: var(--radius-sm);
cursor: pointer;
touch-action: manipulation; /* Phase 88: standard dead-mobile-button fix — the
toggle's taps must not wait on the double-tap-zoom/pinch disambiguation
(owner bug report 2026-09-08: dead hamburger on chat-page cold boots). */
}
.nav-toggle:hover { background: var(--brand-soft); color: var(--brand-ink); }
/* The icon is sized (an unsized inline SVG would default to 300px
and blow the bar out); 20px reads as a proper hamburger inside
the 44px target. */
.nav-toggle svg { width: 20px; height: 20px; display: block; }
/* The nav becomes the dropdown. The containing block is the sticky
.app-header (.header-inner is not positioned), so the menu spans
the header's full width — edge to edge — intended on mobile;
z-index 21 = header (20) + 1, above the bar content. */
.app-nav {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-left: 0;
flex-direction: column;
gap: 0;
background: var(--surface);
border-bottom: 1px solid var(--line);
box-shadow: var(--shadow-lg);
padding: 0.5rem 0;
z-index: 21;
/* Closed state (default): invisible and non-interactive — task 02's
header.js is the only opener (.is-open + aria-expanded). */
visibility: hidden;
opacity: 0;
transform: translateY(-8px);
pointer-events: none;
transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 180ms;
}
.app-nav.is-open {
visibility: visible;
opacity: 1;
transform: none;
pointer-events: auto;
transition: opacity 180ms ease, transform 180ms ease, visibility 0s;
}
/* Phase 88: while the mobile menu is open, the chat's sticky bottom
cluster is hidden — it is the other positioned/layered element on
the page and must not compete for taps with the open menu (it also
overlaps the menu's lower rows on short viewports — menu y58→417
vs cluster top y395 at 390×600). visibility (not display): layout
is preserved, so closing never reflows the chat column. */
body.nav-menu-open .chat-bottom {
visibility: hidden;
}
/* Menu rows: comfortable ≥44px targets (0.75rem × 2 + the 1rem line)
and readable text — replaces the old .nav-link pill squeeze. */
.app-nav .nav-link { padding: 0.75rem 1.25rem; font-size: 1rem; }
.new-chat-btn { padding: 0.4rem 0.3rem; }
.new-chat-label { display: none; }
.new-chat-btn svg { display: block; }
/* Phase 51: the Share pill squeezes with New chat (same family, same
rules — the chat-shell override below keeps the label visible). */
.share-chat-btn { padding: 0.4rem 0.3rem; }
.share-chat-label { display: none; }
.share-chat-btn svg { display: block; }
/* But on the chat page there is room — keep the label visible and
hide the icon (the button lives inside .chat-shell, not the navbar). */
.chat-shell .new-chat-label { display: inline; }
.chat-shell .new-chat-btn svg { display: none; }
.chat-shell .share-chat-label { display: inline; }
.chat-shell .share-chat-btn svg { display: none; }
/* Phase 55 (task 05, TODO.md L6, A5): the action row flips to a
vertical stack at the phone breakpoint — full-width pills, New
chat above Share. The pill rules above (padding, the
icon/label handling, the .chat-shell label overrides) apply to
the stacked pills unchanged. */
.chat-actions { flex-direction: column; align-items: stretch; gap: 0.5rem; }
/* Phase 53: the stale banner's row wraps at phone width (message
above, action below) — the pill takes a full-width comfortable
row instead of squeezing into the text. */
.stale-regenerate { margin-left: 0; width: 100%; }
.banner-retry { margin-left: 0; width: 100%; }
/* Phase 16: the auth pill goes icon-only like New chat — brand text
ellipsizes as the designated squeeze target, no bar overflow. */
.auth-link { padding: 0.4rem 0.3rem; }
.auth-label { display: none; }
.auth-link svg { display: block; }
/* Phase 46 UX revision: sign-out & sign-in move into the hamburger
dropdown on mobile — hide the bar copies, show the dropdown copies. */
.sign-out-mobile { display: inline-flex; }
#sign-out-btn { display: none !important; }
.sign-in-mobile { display: inline-flex; }
#sign-in-link { display: none !important; }
.nav-toggle { margin-left: auto; }
/* Dropdown-style sign-out: full-width row, error palette, label visible. */
#app-nav .sign-out-btn {
display: flex;
width: 100%;
align-items: center;
gap: 0.6rem;
text-align: left;
padding: 0.75rem 1.25rem;
font-size: 1rem;
border: 0;
border-radius: 0;
background: transparent;
color: var(--err-ink);
min-height: 48px;
}
#app-nav .sign-out-btn:hover {
background: var(--err-bg);
color: var(--err-ink);
}
#app-nav .sign-out-btn svg {
width: 18px;
height: 18px;
flex: 0 0 auto;
}
#app-nav .sign-out-btn .auth-label { display: inline; }
/* Dropdown-style sign-in: matches sign-out row styling. */
#app-nav .sign-in-mobile {
display: flex;
width: 100%;
align-items: center;
gap: 0.6rem;
text-align: left;
padding: 0.75rem 1.25rem;
font-size: 1rem;
border: 0;
border-radius: 0;
background: transparent;
color: var(--err-ink);
font-weight: 600;
text-decoration: none;
min-height: 48px;
}
#app-nav .sign-in-mobile:hover {
background: var(--err-bg);
color: var(--err-ink);
}
#app-nav .sign-in-mobile svg {
width: 18px;
height: 18px;
flex: 0 0 auto;
}
#app-nav .sign-in-mobile .auth-label { display: inline; }
.steering-note { padding: 0.3rem 0.3rem 0.3rem 0.7rem; }
.app-main > .steering-panel { width: calc(100% - 1.8rem); }
.tune-btn { min-height: 44px; }
.retry-btn { min-height: 44px; }
.edit-btn { min-height: 44px; }
.continue-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-origin { max-width: 16rem; } /* phase 145 (task 03): the Original anchor squeezes like the path */
.doc-copy-path { min-height: 44px; min-width: 44px; } /* phase 153 (task 02): the Copy path control's ≥44px touch-target floor (PLAN §7.1) */
.doc-md { padding: 1.1rem 1rem; }
.doc-summary { padding: 0.75rem 0.9rem; }
.doc-raw { padding: 1rem; font-size: 0.8rem; }
/* Phase 26: the modal bar squeezes like the other bars — the Full page
pill goes icon-only (aria-label keeps the name), the title clips;
the panel stays 96vw × 92vh, so no horizontal overflow at 360px. */
.doc-modal-header { gap: 0.5rem; padding-inline: 0.9rem; }
.doc-modal-title { font-size: 1.1rem; }
.doc-modal-open { padding: 0.4rem 0.55rem; }
.doc-modal-open svg { display: block; }
.doc-modal-open span { display: none; }
.doc-modal-meta { padding-inline: 0.9rem; }
.doc-modal-content { padding: 0.75rem 0.9rem 1.25rem; }
.composer { padding: 0.5rem; }
/* Phase 35 (phase 49: + the archive upload form): the add forms
stack like the other cards — label, full-width input, full-width
button; the table wrapper's horizontal scroll already covers long
URLs/paths. */
#git-source-form,
#archive-upload-form { flex-direction: column; align-items: stretch; }
#git-source-form > label,
#archive-upload-form > label { white-space: normal; }
#git-source-url,
#git-source-branch,
#git-source-token,
#archive-upload-file { min-width: 0; }
#git-source-add,
#archive-upload-btn { width: 100%; }
/* Phase 50: the History table keeps its full width (the .table-wrap
horizontal scroll already covers it); the actions cell wraps so
the two-step confirm pair fits the phone width. */
.history-actions-cell { white-space: normal; }
.history-actions { flex-wrap: wrap; }
/* Phase 79 task 06: the Tokens create row stacks — the label input
takes the full width, the Generate pill drops under it (full
width); the once field's row stacks the same way; the actions
cell wraps so the two-step confirm pair fits the phone width
(the table's own horizontal scroll covers the columns). */
.token-create { flex-direction: column; align-items: stretch; }
.token-generate { width: 100%; }
.token-once-row { flex-direction: column; align-items: stretch; }
#token-once-copy { width: 100%; }
.tokens-actions-cell { white-space: normal; }
.tokens-actions { flex-wrap: wrap; }
/* Phase 91 task 05: the Theme form squeezes — the color grid drops
to 1 column (the label + swatch pairs keep their ≥44px targets)
and the actions row stacks (Save full width, Reset under it). */
.theme-colors { grid-template-columns: 1fr; }
.theme-actions { flex-direction: column; align-items: stretch; }
#theme-save { width: 100%; }
.theme-reset { width: 100%; }
/* Phase 168 (task 05): the Settings panel squeezes — the search
row stacks (the label over the full-width input) and the action
row stacks (Save full width, Clear all under it; the ≥44px
targets are the global rules above — the stack keeps them on
phones). The actions cell wraps so the Reset control fits the
phone width (the table's own horizontal scroll covers the
columns). */
.settings-search { flex-direction: column; align-items: stretch; }
.settings-actions { flex-direction: column; align-items: stretch; }
#settings-save { width: 100%; }
#settings-clear-all { width: 100%; }
.settings-actions-cell { white-space: normal; }
/* Phase 176 (task 01; TODO L3, owner-locked A2): the Value column
gets a usable floor on the phone. The Setting cells' nowrap
labels (the .settings-table td rule above) exhausted the 560px
table floor, collapsing the min-width: 0 Value cells to 2–3
characters at 375px. The cell's min-width is 13rem + 2rem (the
td's own 1rem × 2 padding — the global border-box model): the
.settings-control (width: 100% of the cell's CONTENT) lands at
the locked 13rem (≈208px ≈ 14 mono chars at the controls'
0.85rem) — readable and editable on a phone; the 720px table
floor + the card's overflow: auto (.table-wrap) mean the phone
scrolls the CARD, never the page (the phase-171 "no horizontal
page overflow" pin holds). 720px is a bare length literal — NO
new CSS custom property (B3 binds palette variables; a length in
the mobile block is the idiom the neighboring width: 100% rules
already use). Mobile-only (A1): at ≥641px these rules are inert
and the desktop layout stays byte-identical. */
.settings-table { min-width: 720px; }
.settings-value-cell, .settings-value-th { min-width: calc(13rem + 2rem); }
/* Phase 51: the shared page squeezes like the chat column — the
title and the note step down (the empty-state-title family); the
shell is full-width here (the 72rem cap never binds on a phone —
the container is already 100%) and .msg-body's 92% override
above applies. */
#shared-title { font-size: 1.35rem; }
.shared-note { font-size: 0.88rem; }
.footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; }
main { padding-bottom: env(safe-area-inset-bottom, 0); }
/* Sync + History refresh: full-width LABELED pills on mobile (the
icon-only squeeze is gone — a symbol-only control is too small a
target to discover on a phone). The RAG page-head row wraps so
the Sync pill drops below the "Knowledge base" title at full
width; the History page-head already wraps the pill below its
title block. The Sync label's min(16rem, 40vw) cap lifts and the
label FILLS the pill (phase 128: flex 1 1 auto) — .sync-label-main
ellipsizes against the width left by the pinned .sync-label-count
(+ the phase-129 .sync-label-chunk part while a file's embed batch
is in flight), so the position parts stay on-screen at the right
edge, never clipped;
the refresh glyph joins its visible label. The aria-labels keep
the accessible names. */
.page-head-row { flex-wrap: wrap; }
.sync-btn { width: 100%; }
.sync-cancel-btn { width: 100%; } /* phase 134: the Cancel pill stacks full-width under the Sync pill (the row already wraps) */
.sync-label { max-width: none; flex: 1 1 auto; min-width: 0; }
.history-refresh, .history-pager-btn { width: 100%; }
.history-refresh svg, .history-pager-btn svg { display: block; }
/* Phase 175 (task 02, D6): the pager row STACKS (the
.settings-actions idiom) so the full-width Prev / Next pills
don't side-by-side overflow the phone width (the pill rule's
flex-shrink: 0 would keep each at 100%); the "Page N of M"
status sits between them (the 0.75rem gap goes vertical). */
.history-pager { flex-direction: column; align-items: center; }
/* Phase 182 (task 06): the two Copy controls of a verbatim panel and of
an ordinary answer code block reach the 44px touch floor on a phone
(the desktop pill is 32px tall — fine for a pointer, too small a tap
target for a thumb). The pill itself grows; its label keeps the
ellipsis escape, so a long "Copy 210 lines from …" never widens the
panel. */
.code-block-copy, .verbatim-copy { min-height: 44px; }
/* Phase 182 (task 07): the panel's viewer link is a control too — it
reaches the same touch floor (inline-flex so the line box grows without
moving the baseline of the caption row it sits in). */
.verbatim-open { min-height: 44px; display: inline-flex; align-items: center; }
}
/* Phase 46: prefers-reduced-motion stills the mobile menu — no
180ms slide+fade; open/close snaps (the visibility/opacity flip
applies instantly) and stays correct. BOTH states are named: the
.is-open rule (0,2,0) would otherwise out-specify a bare .app-nav
(0,1,0) and the OPEN transition would still animate. */
@media (prefers-reduced-motion: reduce) {
.app-nav,
.app-nav.is-open { transition: none; }
}
. */ .kb-level-body .kb-summary-edit { margin-top: 0.5rem; } .kb-summary-editor { display: block; width: 100%; min-height: 8rem; /* the phase-57 spec */ padding: 0.6rem 0.8rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--bg); /* inset against the --surface panel / row */ color: var(--ink); /* 16.7:1 on --bg (AA) */ font: inherit; line-height: 1.5; resize: vertical; } /* Phase 99 (task 01): the open editor fills the .kb-desc-cell flex wrapper — as a flex item it needs the explicit grow + min-width: 0 (the width: 100% base above still serves the level block's non-flex .kb-level-body). */ .kb-desc-cell .kb-summary-editor { flex: 1 1 auto; min-width: 0; } .kb-summary-actions { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.75rem; } .kb-summary-save { display: inline-flex; align-items: center; min-height: 32px; padding: 0.35rem 0.95rem; border: 0; border-radius: 999px; background: var(--brand); color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */ font: inherit; font-weight: 600; font-size: 0.85rem; cursor: pointer; } .kb-summary-save:hover { background: var(--brand-hover); } /* the house hover lightening */ .kb-summary-save:disabled { opacity: 0.6; cursor: default; } /* one PATCH at a time */ .kb-summary-cancel { display: inline-flex; align-items: center; min-height: 32px; padding: 0.35rem 0.95rem; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ font: inherit; font-weight: 600; font-size: 0.85rem; cursor: pointer; } .kb-summary-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); } .kb-summary-status { margin: 0.6rem 0 0; font-size: 0.85rem; color: var(--ink-soft); /* 5.1:1 on --surface (AA) */ } .kb-summary-status:empty { margin-top: 0; } /* Phase 98 (task 04, D4): the row-cell "Summary pending" marker — the muted ink-soft pair (5.1:1 on --surface, AA); text + color, never color alone. The class sits on the EXISTING description text span, so the row cell's font-size/line-height apply — the marker must not change row height (no font/white-space overrides). No italic, no new hue (the phase-92 monochrome invariant). */ .kb-summary-pending { color: var(--ink-soft); } /* ---------- Git sources page (phase 35) ---------- /git-sources.html: the admin-only manager for the stored git source list (add / remove, git-sources table). Same full-width table language as the Sources page (no skinny single-column list), the phase-16 gate reused verbatim (.sources-gate classes), and the phase-27 form-card language for the add form. Every pair reuses the Phase-08 AA palette: dark ink on brand 5.2:1 (never white on brand, 3.7:1, fails), brand-ink/brand-soft 6.9:1, err 9.1:1, ink-soft >=6.9:1. Touch targets >=44px; :focus-visible via the global 3px outline rule. No filter: blur, no CDN, system fonts. */ .git-sources-shell { display: flex; flex-direction: column; gap: 1.25rem; flex: 1; } /* Phase 174 (owner bug report 2026-10-21): the shell's gap only separates the gate from this wrapper — every visible card (page-head, the load-error alert, the env note, the two form cards, the table card) is a child of THIS div, which was a plain block, so the cards stacked flush (1px borders touching). The phase-171 #settings-content idiom: the wrapper itself is the rhythm column, reusing the shell's own 1.25rem token — no new spacing value. The hidden children (#git-sources-load-error, #git-sources-env-note — the env note's CLASS is the singular git-source-env-note) keep the UA [hidden] display:none inside a flex parent, so git-sources.js's reveal lifecycle is untouched; a revealed one joins the rhythm. */ #git-sources-content { display: flex; flex-direction: column; gap: 1.25rem; } /* Add forms — the tuning form's surface as a single row: visible label + mono location input (git URLs may embed credentials, so the input is mono) + the brand button; wraps to a column at narrow widths (the <=640px block below). Phase 49: the archive upload form (#archive-upload-form, replacing the phase-38 local-directory form) reuses the git form's card + button rules VERBATIM — one form language for both; its file input carries its own rules below. */ #git-source-form, #archive-upload-form { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 0.9rem 1rem 1rem; } #git-source-form:focus-within, #archive-upload-form:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); } #git-source-form > label, #archive-upload-form > label { color: var(--ink); font-weight: 600; white-space: nowrap; } /* Phase 121 (task 03): the form-label "optional" hint — the muted ink-soft pair (5.1:1 on the label's surface, AA) at the label's 600 weight relaxed to 400 so the hint reads as secondary (it qualifies the name, it is not the name); small, inline. */ .field-hint { color: var(--ink-soft); font-weight: 400; font-size: 0.8rem; } /* #git-source-token (phase 121): the optional masked private-repo credential — the URL input's treatment VERBATIM (mono, >=44px target, brand focus); type=password masks the value in display. */ #git-source-url, #git-source-branch, #git-source-token { flex: 1; min-width: 14rem; min-height: 44px; font-family: var(--mono); font-size: 0.88rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.45rem 0.7rem; } #git-source-url::placeholder, #git-source-branch::placeholder, #git-source-token::placeholder { color: var(--ink-soft); } #git-source-url:focus-visible, #git-source-branch:focus-visible, #git-source-token:focus-visible { outline-offset: 0; border-color: var(--brand); } #git-source-add, #archive-upload-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0.4rem 1.2rem; border: 0; border-radius: var(--radius-sm); background: var(--brand); color: var(--bg); /* dark ink on brand: 5.2:1 */ font: inherit; font-weight: 700; cursor: pointer; } #git-source-add:hover:not(:disabled), #archive-upload-btn:hover:not(:disabled) { background: var(--brand-hover); } #git-source-add:disabled, #archive-upload-btn:disabled { opacity: 0.6; cursor: wait; } /* Phase 49: the upload form's file control — mono-ish, on-surface, a >=44px touch target, :focus-visible via the global 3px outline rule (offset zeroed + brand border, exactly like the git URL input). The chosen filename renders in mono; the selector button keeps a plain surface chip. */ #archive-upload-file { flex: 1; min-width: 14rem; min-height: 44px; font-family: var(--mono); font-size: 0.88rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 0.3rem 0.5rem; } #archive-upload-file:focus-visible { outline-offset: 0; border-color: var(--brand); } #archive-upload-file::file-selector-button { min-height: 34px; margin-right: 0.6rem; padding: 0.3rem 0.9rem; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); /* 13.8:1 on surface */ font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer; } #archive-upload-file::file-selector-button:hover { border-color: var(--brand); color: var(--brand-ink); } /* The upload's success line (role=status): the .sync-progress-text soft-ink result-line language ("2 added · 1 pruned") — ink-soft on surface (>=4.5:1), the dashed hint-box border marks it as a result, not an error; it drops onto its own row under the input like the error line. */ #archive-upload-result { flex-basis: 100%; margin: 0; color: var(--ink-soft); background: var(--surface); border: 1px dashed var(--line); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-size: 0.85rem; font-weight: 600; } /* The add form's inline error (role=alert): the err pair (9.1:1); flex-basis 100% drops it onto its own row under the input. */ .git-source-error { flex-basis: 100%; margin: 0; background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius-sm); padding: 0.45rem 0.8rem; font-size: 0.85rem; font-weight: 600; } /* Load failure (role=alert) + the retry action: a failed GET /api/git-sources must never leave a stuck page. The retry button keeps the err pair on the err surface (9.1:1). */ .git-source-load-error { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; background: var(--err-bg); color: var(--err-ink); border: 1px solid var(--err-line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 0.7rem 0.9rem; font-size: 0.9rem; font-weight: 600; } .git-source-load-error > span { flex: 1; min-width: 12rem; } #git-sources-retry { min-height: 44px; padding: 0.4rem 1rem; border: 1px solid var(--err-line); border-radius: var(--radius-sm); background: transparent; color: var(--err-ink); font: inherit; font-weight: 700; cursor: pointer; } #git-sources-retry:hover { background: color-mix(in srgb, var(--err-line) 12%, transparent); } /* Env-fallback note (from_env: true — the table is empty and the list is BOR_GIT_SOURCES): the info chip in the theme palette — brand-soft surface, brand-ink text (6.9:1). */ .git-source-env-note { margin: 0; background: var(--brand-soft); color: var(--brand-ink); border: 1px solid var(--brand-soft); border-radius: var(--radius-sm); padding: 0.6rem 0.9rem; font-size: 0.88rem; } .git-source-env-note code { font-family: var(--mono); font-size: 0.85em; background: var(--surface); color: var(--brand-ink); /* 8.7:1 on surface */ padding: 0.1em 0.35em; border-radius: 5px; } /* Hint box (role=note): the page-sub styling family — ink-soft on surface (6.9:1), a dashed border marks it as guidance, not state. It names the Sync button: the action that clones the listed repos and prunes the removed ones (the phase scope boundary). */ .git-source-hint { margin: 0; color: var(--ink-soft); background: var(--surface); border: 1px dashed var(--line); border-radius: var(--radius-sm); padding: 0.6rem 0.9rem; font-size: 0.88rem; } /* ---------- Remove confirmation modal (phase 69, task 02) ---------- /git-sources.html: the in-app confirmation that replaces the native confirm() — the total-removal warning (the row, the source's indexed documents, and — for git clones and uploaded archives — the files on the server's disk). The .doc-modal overlay contract: a fixed full-viewport dim backdrop + a centered panel (z-index 1000, above the sticky header (20) + skip-link (100); NO blur — the phase-08 no-blur perf anchor), scaled to a compact dialog: the chat-column width (the --chat-column token — 72rem, the .container width) or the viewport, whichever is narrower. Phase-08 tokens only; system fonts; no CDN. AA pairs: title/copy are --ink on --surface (13.8:1); the source value is --ink on --bg (16.7:1); the error line is the err pair (err-ink on err-bg 9.3:1, the err-line border); the destructive button rides the err token family — the .tuning-delete / .steering-delete hover convention used as the resting state (err-ink on err-bg 9.3:1; the hover inverts to dark --bg on --err-line, 5.2:1 — both AA); Cancel is the ghost ink-soft family (5.1:1 on --surface). :focus-visible via the global 3px outline rule (no local suppression — focus lands on Cancel at open and is visible); both buttons >=44px; no animation (reduced-motion safe by construction). */ .remove-confirm { position: fixed; inset: 0; z-index: 1000; display: flex; /* the panel is the only in-flow child — margin: auto centers it */ } /* Explicit (the global [hidden] rule already wins — the documented, testable contract for the skeleton). */ .remove-confirm[hidden] { display: none; } .remove-confirm-backdrop { position: fixed; inset: 0; /* --bg at 82% — the doc-modal dim, no backdrop-filter (no-blur). */ background: color-mix(in srgb, var(--bg) 82%, transparent); } .remove-confirm-panel { /* position:relative lifts the panel above the fixed backdrop (positioned elements paint over in-flow siblings otherwise). */ position: relative; z-index: 1; margin: auto; width: min(var(--chat-column), calc(100vw - 2rem)); padding: 1.5rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-lg); } .remove-confirm-title { margin: 0 0 0.75rem; font-size: 1.25rem; line-height: 1.3; color: var(--ink); } /* The source's value (git URL or local path) — mono, wrapped (a long URL must not overflow the panel), --ink on --bg (16.7:1). */ .remove-confirm-source { display: block; margin: 0 0 0.75rem; padding: 0.5rem 0.65rem; font-family: var(--mono); font-size: 0.85rem; line-height: 1.5; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); overflow-wrap: anywhere; } .remove-confirm-copy { margin: 0; color: var(--ink); /* 13.8:1 on --surface */ } /* The in-modal failure line (role=alert): the err pair (err-ink on err-bg 9.3:1, the err-line border) — the .git-source-error banner language, boxed. */ .remove-confirm-error { margin: 0.75rem 0 0; padding: 0.5rem 0.65rem; color: var(--err-ink); background: var(--err-bg); border: 1px solid var(--err-line); border-radius: var(--radius-sm); } .remove-confirm-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1.1rem; } /* The two modal buttons: >=44px targets, the house 3px :focus-visible via the global outline rule (no local override). */ .remove-confirm-btn { min-height: 44px; min-width: 44px; padding: 0.55rem 1.1rem; border-radius: var(--radius-sm); font: inherit; font-weight: 600; font-size: 0.9rem; white-space: nowrap; cursor: pointer; } .remove-confirm-btn:disabled { opacity: 0.5; cursor: wait; } /* Cancel — the ghost ink-soft family (5.1:1 on --surface), like the row's Remove / .tuning-edit; the brand pair on hover (6.9:1). */ .remove-confirm-cancel { border: 1px solid var(--line); background: transparent; color: var(--ink-soft); } .remove-confirm-cancel:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); } /* "Remove source" — the destructive button on the err token family (the .tuning-delete / .steering-delete convention): err-ink on err-bg 9.3:1, the err-line border; the hover inverts to dark --bg on --err-line (5.2:1 — AA). */ .remove-confirm-remove { border: 1px solid var(--err-line); background: var(--err-bg); color: var(--err-ink); } .remove-confirm-remove:hover:not(:disabled) { background: var(--err-line); color: var(--bg); } /* ---------- Per-source ignore-paths editor (phase 89, task 05) ---------- The shell's Sources view: the page-local alertdialog that edits one source's ignore list (one path per line — the phase-89 A1 prefix rule, stated in plain words in the helper copy). The EXACT #remove-confirm-dialog overlay contract (phase 69): a fixed full-viewport dim backdrop + a centered panel (z-index 1000, above the sticky header (20) + skip-link (100); NO blur — the phase-08 no-blur perf anchor), scaled to the chat-column width (the --chat-column token — 72rem, the .container width) or the viewport, whichever is narrower. The box: a VISIBLE block label (WCAG — never aria-label-only) over a mono textarea (the box is a data entry, not prose — the var(--mono) stack); the error line is the err pair (err-ink on err-bg 9.3:1, the err-line border); Cancel is the ghost ink-soft family (5.1:1 on --surface); Save is the solid brand family (--bg text on --brand 5.2:1 — the .new-chat-btn convention; the hover lightens the fill). Both buttons >=44px; :focus-visible via the global 3px outline rule (no local suppression — focus lands on Cancel at open and is visible); no animation (reduced-motion safe by construction). AA pairs: title / copy / label are --ink (or ink-soft) on --surface (13.8:1 / 5.1:1); the source value is --ink on --bg (16.7:1). Phase-08 tokens only; system fonts; no CDN. */ .ignore-editor { position: fixed; inset: 0; z-index: 1000; display: flex; /* the panel is the only in-flow child — margin: auto centers it */ } /* Explicit (the global [hidden] rule already wins — the documented, testable contract for the skeleton). */ .ignore-editor[hidden] { display: none; } .ignore-editor-backdrop { position: fixed; inset: 0; /* --bg at 82% — the doc-modal dim, no backdrop-filter (no-blur). */ background: color-mix(in srgb, var(--bg) 82%, transparent); } .ignore-editor-panel { /* position:relative lifts the panel above the fixed backdrop (positioned elements paint over in-flow siblings otherwise). */ position: relative; z-index: 1; margin: auto; width: min(var(--chat-column), calc(100vw - 2rem)); padding: 1.5rem; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-lg); } .ignore-editor-title { margin: 0 0 0.75rem; font-size: 1.25rem; line-height: 1.3; color: var(--ink); } /* The source's value (git URL or local path) — mono, wrapped (a long URL must not overflow the panel), --ink on --bg (16.7:1). */ .ignore-editor-source { display: block; margin: 0 0 0.75rem; padding: 0.5rem 0.65rem; font-family: var(--mono); font-size: 0.85rem; line-height: 1.5; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); overflow-wrap: anywhere; } .ignore-editor-copy { margin: 0 0 1rem; color: var(--ink); /* 13.8:1 on --surface */ } .ignore-editor-copy code { font-family: var(--mono); font-size: 0.85em; } /* Visible block label (WCAG — the textarea is NEVER labelled only via aria-label); ink-soft on --surface (5.1:1). */ .ignore-editor-label { display: block; margin-bottom: 0.35rem; color: var(--ink-soft); font-size: 0.88rem; font-weight: 600; } /* The box: full panel width, mono (the var(--mono) stack), theme- consistent border on --bg; the focus-visible ring is the global 3px outline (no local suppression); vertical resize for long lists (up to 200 entries, A4). */ .ignore-editor-textarea { display: block; width: 100%; min-height: 9rem; padding: 0.55rem 0.65rem; font-family: var(--mono); font-size: 0.85rem; line-height: 1.5; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); resize: vertical; } /* Phase 121 (task 03): the editor's masked token field — the textarea's box language (full panel width, --bg fill, the line border, mono) at the 44px touch floor; the label's top margin keeps the field pair off the textarea. type=password + autocomplete=off live in the markup (a PAT is not a site credential — no browser save offer); the focus ring is the global 3px outline rule. */ .ignore-editor-token-label { margin-top: 0.9rem; } .ignore-editor-token { display: block; width: 100%; min-height: 44px; padding: 0.55rem 0.65rem; font-family: var(--mono); font-size: 0.85rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm); } /* Phase 135 (task 04): the editor's branch field — the token field's box language (full panel width, --bg fill, the line border, mono) at the 44px touch floor; type=text (a branch is a name, not a credential — no masking) + autocomplete=off live in the markup, and the label reuses .ignore-editor-label (the source line's 0.75rem bottom margin is the field pair's top gap — no extra margin class). The muted hint line carries the .field-hint values (ink-soft, 400, 0.8rem — the token field's hint styling) as a standalone
; the
bottom margin is the gap to the copy line (which has no top margin).
Git rows only — git-sources.js hides the group for local rows. */
#ignore-editor-branch {
display: block;
width: 100%;
min-height: 44px;
padding: 0.55rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
}
.ignore-editor-branch-hint {
margin: 0.35rem 0 1rem;
color: var(--ink-soft);
font-size: 0.8rem;
}
/* The in-dialog failure line (role=alert): the err pair (err-ink on
err-bg 9.3:1, the err-line border) — the .remove-confirm-error
language. */
.ignore-editor-error {
margin: 0.75rem 0 0;
padding: 0.5rem 0.65rem;
color: var(--err-ink);
background: var(--err-bg);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
}
.ignore-editor-actions {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
margin-top: 1.1rem;
}
/* The two dialog buttons: >=44px targets, the house 3px :focus-visible
via the global outline rule (no local override). */
.ignore-editor-btn {
min-height: 44px;
min-width: 44px;
padding: 0.55rem 1.1rem;
border-radius: var(--radius-sm);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
.ignore-editor-btn:disabled { opacity: 0.5; cursor: wait; }
/* Cancel — the ghost ink-soft family (5.1:1 on --surface), like the
remove dialog's Cancel; the brand pair on hover (12.4:1 on
brand-soft). */
.ignore-editor-cancel {
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
}
.ignore-editor-cancel:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
/* "Save" — the solid brand family (the .new-chat-btn convention):
--bg text on --brand (5.2:1 — AA); the hover lightens the brand
fill (the .new-chat-btn hover pair). */
.ignore-editor-save {
border: 0;
background: var(--brand);
color: var(--bg);
}
.ignore-editor-save:hover:not(:disabled) { background: var(--brand-hover); }
/* The list: the Sources page's table pattern — full width in the
72rem frame. The URL column never wraps or ellipsizes: long
URLs, credentials included, scroll the wrapper instead of
truncating (the .git-sources-table 560px min-width keeps the
in-wrapper horizontal scroll). Phase 156 (task 01): the id
card's duplicate background/border/radius/shadow/overflow
declarations were REMOVED — the wrapper carries
class="table-wrap", so it inherits the base card + the
bounded-panel max-height + the sticky rule (the one-rule
contract: no divergent unbounded override). */
.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 {
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* 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;
/* Phase 156 (task 02, A3): the width support for the "Remove"
label span's clamp (same rationale as .git-source-hidden
above). */
max-width: 100%;
min-height: 44px;
min-width: 44px;
flex: 0 0 auto;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.git-source-remove svg { width: 14px; height: 14px; display: block; }
.git-source-remove:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.git-source-remove:disabled { opacity: 0.5; cursor: wait; }
/* Phase 89: the per-row "Ignore paths" trigger (makeRow — left of
Remove): the .git-source-remove idiom (same size/spacing, >=44px
target) in a NEUTRAL secondary fill — it must read distinct from
the destructive Remove (text label included, never icon-only; the
hover takes the brand-soft pair, 12.4:1 — Remove hovers to the err
pair, 9.3:1). */
.git-source-ignore {
display: inline-flex;
align-items: center;
justify-content: center;
/* Phase 156 (task 02, A3): the width support for the "Ignore
paths" label span's clamp (same rationale as .git-source-hidden
above). */
max-width: 100%;
min-height: 44px;
min-width: 44px;
flex: 0 0 auto;
padding: 0.35rem 0.7rem;
margin-right: 0.4rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.git-source-ignore:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
/* Phase 89: the "N ignored" count tag — next to the location
in the Source cell (TEXT + a distinct background, never color
alone — WCAG 1.4.1): --ink on --bg (16.7:1) inside the --surface
card; the line border keeps the chip legible when the row hover
swaps the cell onto --bg too. */
.git-source-ignore-count {
display: inline-block;
margin-left: 0.55rem;
padding: 0.08rem 0.5rem;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
color: var(--ink);
background: var(--bg);
white-space: nowrap;
}
/* Phase 105 (A5): the per-row "Hidden" checkbox — the actions
cell, LEFT of the "Ignore paths" button (JS builds it,
git-sources.js makeRow). A native labeled checkbox: the
visible "Hidden" text + the checkbox's own aria-label
(full source value). ~44px hit height matches the action
buttons; :focus-visible via the GLOBAL rule (L146) — no
per-control ring needed. */
.git-source-hidden {
display: inline-flex;
align-items: center;
gap: 0.4rem;
/* Phase 156 (task 02, A3): the width support for the "Hidden"
label span's clamp (the label is an inline-flex child of the
cell — clamped to the cell so the span can ellipsize in a
squeezed column instead of overflowing the card edge). */
max-width: 100%;
height: 44px;
padding: 0 0.5rem;
font-size: 0.8rem;
color: var(--ink);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
/* --brand checkbox fill: the native check renders in --bg on
--brand — VERIFIED (phase 105): built-in dark theme
#0f0a0a on #f43f5e = 5.2:1 (the house brand pairing;
measured 5.35:1 — WCAG AA ≥4.5:1); every theme-tab-saved
palette is gated by the tab's eight-pair contrast check
(theme.js PAIRS — "bg" on "brand" at AA_MIN 4.5), so the
checked-state ratio stays AA in all themes. */
.git-source-hidden input[type="checkbox"] {
width: 1.05rem;
height: 1.05rem;
margin: 0;
accent-color: var(--brand);
cursor: pointer;
}
.git-source-hidden:disabled { opacity: 0.5; cursor: wait; }
/* Phase 105 (A5): the "hidden on" state tag — a copy of the
.git-source-ignore-count idiom (TEXT + background, never
color alone — WCAG 1.4.1: --ink on --bg 16.7:1). Phase 135 (A6):
the "branch: