+ the Actions header span in #view-history /
#view-tokens). Without a positioned ancestor their 1px boxes lay
out against the INITIAL containing block — at the 640px table's
right edge — and leak into the DOCUMENT's scrollable overflow
(the page appeared "the width of the table", TODO.md L4; the
Tokens view carried the identical defect). Zero-offset positioning
changes no layout: the spans stay clipped by their own
clip: rect(0 0 0 0) + 1px box, and cards without such spans (the
RAG .docs-table, the git-sources table) render unchanged. */
.table-wrap {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow-x: auto;
}
.docs-table {
width: 100%;
border-collapse: collapse;
min-width: 640px;
font-size: 0.93rem;
}
.docs-table th, .docs-table td {
text-align: left;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
white-space: nowrap;
}
.docs-table th {
background: var(--brand-soft);
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
position: sticky;
top: 0;
}
.docs-table td:nth-child(2) { font-family: var(--mono); font-size: 0.82rem; max-width: 30rem; overflow: hidden; text-overflow: ellipsis; }
.docs-table tbody tr:hover { background: var(--bg); }
.docs-table tbody tr:last-child td { border-bottom: 0; }
/* ---------- KB drill-down tree (phase 97, task 04) ----------
The RAG view lists the catalog the way the agent's `ls` sees it
(the phase-94 concept, one end to end): #kb-crumb (the location
breadcrumb — hidden at the top level), #kb-level (the current
directory's STORED description — the surface-panel language of the
phase-93 page heads), and #folders-table (the ONE folders/sources
table — the .docs-table language; .kb-folders-table only re-styles
the 2nd/4th column cells — phase 106 (task 08, D8) moved
Description from 3rd to 4th, the Updated column taking 3rd). Phase-08
tokens only — NO new hue
(the phase-92 monochrome invariant): brand-ink on surface 9.0:1,
ink 13.8:1, ink-soft 5.1:1 (every pair AA). :focus-visible via the
global 3px outline rule; row targets keep the .docs-table cell
padding (>= 44px effective — the phase-08 touch floor). No CDN,
system fonts. */
.kb-crumb {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.1rem 0.3rem;
margin: 0.9rem 0 0;
font-size: 0.92rem;
}
.kb-crumb-link {
color: var(--brand-ink); /* 10.4:1 on --bg, 9.5:1 on the --brand-soft hover */
text-decoration: none;
padding: 0.25rem 0.35rem;
border-radius: var(--radius-sm);
}
.kb-crumb-link:hover { background: var(--brand-soft); text-decoration: underline; }
.kb-crumb-current {
color: var(--ink); /* 16.7:1 on --bg */
font-weight: 600;
padding: 0.25rem 0.35rem;
}
.kb-crumb-sep { color: var(--ink-soft); }
.kb-level {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem 1.25rem;
margin: 0.9rem 0 0.75rem;
}
.kb-level h2 {
margin: 0 0 0.35rem;
font-family: var(--mono);
font-size: 1rem;
color: var(--brand-ink); /* 9.0:1 on --surface — the path, in the path voice */
word-break: break-all;
}
.kb-level p { margin: 0; color: var(--ink-soft); } /* 5.1:1 on --surface */
.kb-folders-table { min-width: 480px; }
.kb-folders-table td:nth-child(2) { font-family: var(--font); font-size: inherit; max-width: none; }
/* Phase 106 (task 08, D8): the Updated column sits at 3rd (BETWEEN
Documents and Description) — it takes the .docs-table cell defaults
(nowrap text, the table ink: var(--ink) on the wrap's --surface —
13.8:1, the pair recorded at :root). NO new CSS: the one-line clamp
below follows the Description cell, which moved to 4th. */
.kb-folders-table td:nth-child(4) {
white-space: nowrap; /* phase 99 (D1): one line — the clamp's budget is the column width */
min-width: 18rem;
max-width: 44rem;
font-size: 0.88rem;
}
/* Phase 99 (task 01, D1): the Description cell is ONE flex row — the
text span flexes + ellipsizes (the row height is one line, independent
of the description's length), the Edit button stays fixed (flex: 0 0
auto — it never wraps to a second line). The gap replaces the button's
old margin-left (the context-spacing rule below keeps only the level
block's margin-top). The clamp is visual only: the full text stays in
the DOM + on the span's title (hover). */
.kb-desc-cell {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
}
.kb-desc-text {
flex: 1 1 auto;
min-width: 0; /* lets the flex item shrink — what engages the ellipsis (the .sync-label pattern) */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.folder-link {
color: var(--brand-ink); /* 9.0:1 on --surface (the .doc-link pair, row variant) */
text-decoration: none;
border-radius: var(--radius-sm);
}
.folder-link:hover, .folder-link:focus-visible {
background: var(--brand-soft);
text-decoration: underline;
}
/* 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;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
letter-spacing: 0.02em;
cursor: pointer;
}
.kb-summary-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
.kb-summary-edit[hidden] { display: none; } /* the hidden attr must beat the display above */
/* Context spacing: the row cell's button spacing comes from the
.kb-desc-cell gap (phase 99, task 01 — the margin-left is gone);
the level block's sits below the . */
.kb-level-body .kb-summary-edit { margin-top: 0.5rem; }
.kb-summary-editor {
display: block;
width: 100%;
min-height: 8rem; /* the phase-57 spec */
padding: 0.6rem 0.8rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg); /* inset against the --surface panel / row */
color: var(--ink); /* 16.7:1 on --bg (AA) */
font: inherit;
line-height: 1.5;
resize: vertical;
}
/* Phase 99 (task 01): the open editor fills the .kb-desc-cell flex
wrapper — as a flex item it needs the explicit grow + min-width: 0
(the width: 100% base above still serves the level block's non-flex
.kb-level-body). */
.kb-desc-cell .kb-summary-editor { flex: 1 1 auto; min-width: 0; }
.kb-summary-actions {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.75rem;
}
.kb-summary-save {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0.35rem 0.95rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */
font: inherit;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
}
.kb-summary-save:hover { background: var(--brand-hover); } /* the house hover lightening */
.kb-summary-save:disabled { opacity: 0.6; cursor: default; } /* one PATCH at a time */
.kb-summary-cancel {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0.35rem 0.95rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
}
.kb-summary-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.kb-summary-status {
margin: 0.6rem 0 0;
font-size: 0.85rem;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
}
.kb-summary-status:empty { margin-top: 0; }
/* Phase 98 (task 04, D4): the row-cell "Summary pending" marker — the
muted ink-soft pair (5.1:1 on --surface, AA); text + color, never
color alone. The class sits on the EXISTING description text span,
so the row cell's font-size/line-height apply — the marker must not
change row height (no font/white-space overrides). No italic, no
new hue (the phase-92 monochrome invariant). */
.kb-summary-pending { color: var(--ink-soft); }
/* ---------- Git sources page (phase 35) ----------
/git-sources.html: the admin-only manager for the stored git source
list (add / remove, git-sources table). Same full-width table
language as the Sources page (no skinny single-column list), the
phase-16 gate reused verbatim (.sources-gate classes), and the
phase-27 form-card language for the add form. Every pair reuses the
Phase-08 AA palette: dark ink on brand 5.2:1 (never white on
brand, 3.7:1, fails), brand-ink/brand-soft 6.9:1, err 9.1:1,
ink-soft >=6.9:1. Touch targets >=44px; :focus-visible via the
global 3px outline rule. No filter: blur, no CDN, system fonts. */
.git-sources-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* Add forms — the tuning form's surface as a single row: visible
label + mono location input (git URLs may embed credentials, so the
input is mono) + the brand button; wraps to a column at narrow
widths (the <=640px block below). Phase 49: the archive upload form
(#archive-upload-form, replacing the phase-38 local-directory form)
reuses the git form's card + button rules VERBATIM — one form
language for both; its file input carries its own rules below. */
#git-source-form,
#archive-upload-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 0.9rem 1rem 1rem;
}
#git-source-form:focus-within,
#archive-upload-form:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); }
#git-source-form > label,
#archive-upload-form > label { color: var(--ink); font-weight: 600; white-space: nowrap; }
/* Phase 121 (task 03): the form-label "optional" hint — the muted
ink-soft pair (5.1:1 on the label's surface, AA) at the label's
600 weight relaxed to 400 so the hint reads as secondary (it
qualifies the name, it is not the name); small, inline. */
.field-hint { color: var(--ink-soft); font-weight: 400; font-size: 0.8rem; }
/* #git-source-token (phase 121): the optional masked private-repo
credential — the URL input's treatment VERBATIM (mono, >=44px
target, brand focus); type=password masks the value in display. */
#git-source-url,
#git-source-token {
flex: 1;
min-width: 14rem;
min-height: 44px;
font-family: var(--mono);
font-size: 0.88rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.7rem;
}
#git-source-url::placeholder,
#git-source-token::placeholder { color: var(--ink-soft); }
#git-source-url:focus-visible,
#git-source-token:focus-visible { outline-offset: 0; border-color: var(--brand); }
#git-source-add,
#archive-upload-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0.4rem 1.2rem;
border: 0;
border-radius: var(--radius-sm);
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 */
font: inherit;
font-weight: 700;
cursor: pointer;
}
#git-source-add:hover:not(:disabled),
#archive-upload-btn:hover:not(:disabled) { background: var(--brand-hover); }
#git-source-add:disabled,
#archive-upload-btn:disabled { opacity: 0.6; cursor: wait; }
/* Phase 49: the upload form's file control — mono-ish, on-surface, a
>=44px touch target, :focus-visible via the global 3px outline rule
(offset zeroed + brand border, exactly like the git URL input). The
chosen filename renders in mono; the selector button keeps a plain
surface chip. */
#archive-upload-file {
flex: 1;
min-width: 14rem;
min-height: 44px;
font-family: var(--mono);
font-size: 0.88rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.3rem 0.5rem;
}
#archive-upload-file:focus-visible { outline-offset: 0; border-color: var(--brand); }
#archive-upload-file::file-selector-button {
min-height: 34px;
margin-right: 0.6rem;
padding: 0.3rem 0.9rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink); /* 13.8:1 on surface */
font: inherit;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
}
#archive-upload-file::file-selector-button:hover { border-color: var(--brand); color: var(--brand-ink); }
/* The upload's success line (role=status): the sync-result shape
("2 added · 1 pruned") — ink-soft on surface (>=4.5:1), the dashed
hint-box border marks it as a result, not an error; it drops onto
its own row under the input like the error line. */
#archive-upload-result {
flex-basis: 100%;
margin: 0;
color: var(--ink-soft);
background: var(--surface);
border: 1px dashed var(--line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
}
/* The add form's inline error (role=alert): the err pair (9.1:1);
flex-basis 100% drops it onto its own row under the input. */
.git-source-error {
flex-basis: 100%;
margin: 0;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
}
/* Load failure (role=alert) + the retry action: a failed
GET /api/git-sources must never leave a stuck page. The retry
button keeps the err pair on the err surface (9.1:1). */
.git-source-load-error {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 0.7rem 0.9rem;
font-size: 0.9rem;
font-weight: 600;
}
.git-source-load-error > span { flex: 1; min-width: 12rem; }
#git-sources-retry {
min-height: 44px;
padding: 0.4rem 1rem;
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--err-ink);
font: inherit;
font-weight: 700;
cursor: pointer;
}
#git-sources-retry:hover { background: color-mix(in srgb, var(--err-line) 12%, transparent); }
/* Env-fallback note (from_env: true — the table is empty and the list
is BOR_GIT_SOURCES): the info chip in the theme palette —
brand-soft surface, brand-ink text (6.9:1). */
.git-source-env-note {
margin: 0;
background: var(--brand-soft);
color: var(--brand-ink);
border: 1px solid var(--brand-soft);
border-radius: var(--radius-sm);
padding: 0.6rem 0.9rem;
font-size: 0.88rem;
}
.git-source-env-note code {
font-family: var(--mono);
font-size: 0.85em;
background: var(--surface);
color: var(--brand-ink); /* 8.7:1 on surface */
padding: 0.1em 0.35em;
border-radius: 5px;
}
/* Hint box (role=note): the page-sub styling family — ink-soft on
surface (6.9:1), a dashed border marks it as guidance, not state.
It names the Sync button: the action that clones the listed repos
and prunes the removed ones (the phase scope boundary). */
.git-source-hint {
margin: 0;
color: var(--ink-soft);
background: var(--surface);
border: 1px dashed var(--line);
border-radius: var(--radius-sm);
padding: 0.6rem 0.9rem;
font-size: 0.88rem;
}
/* ---------- Remove confirmation modal (phase 69, task 02) ----------
/git-sources.html: the in-app confirmation that replaces the
native confirm() — the total-removal warning (the row, the
source's indexed documents, and — for git clones and uploaded
archives — the files on the server's disk). The .doc-modal overlay contract:
a fixed full-viewport dim backdrop + a centered panel (z-index
1000, above the sticky header (20) + skip-link (100); NO blur —
the phase-08 no-blur perf anchor), scaled to a compact dialog:
the chat-column width (the --chat-column token — 72rem, the
.container width) or the viewport, whichever is narrower.
Phase-08 tokens only; system fonts; no CDN.
AA pairs: title/copy are --ink on --surface (13.8:1); the source
value is --ink on --bg (16.7:1); the error line is the err pair
(err-ink on err-bg 9.3:1, the err-line border); the destructive
button rides the err token family — the .tuning-delete /
.steering-delete hover convention used as the resting state
(err-ink on err-bg 9.3:1; the hover inverts to dark --bg on
--err-line, 5.2:1 — both AA); Cancel is the ghost ink-soft family
(5.1:1 on --surface). :focus-visible via the global 3px outline
rule (no local suppression — focus lands on Cancel at open and is
visible); both buttons >=44px; no animation (reduced-motion safe
by construction). */
.remove-confirm {
position: fixed;
inset: 0;
z-index: 1000;
display: flex; /* the panel is the only in-flow child — margin: auto centers it */
}
/* Explicit (the global [hidden] rule already wins — the documented,
testable contract for the skeleton). */
.remove-confirm[hidden] { display: none; }
.remove-confirm-backdrop {
position: fixed;
inset: 0;
/* --bg at 82% — the doc-modal dim, no backdrop-filter (no-blur). */
background: color-mix(in srgb, var(--bg) 82%, transparent);
}
.remove-confirm-panel {
/* position:relative lifts the panel above the fixed backdrop
(positioned elements paint over in-flow siblings otherwise). */
position: relative;
z-index: 1;
margin: auto;
width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
}
.remove-confirm-title {
margin: 0 0 0.75rem;
font-size: 1.25rem;
line-height: 1.3;
color: var(--ink);
}
/* The source's value (git URL or local path) — mono, wrapped (a long
URL must not overflow the panel), --ink on --bg (16.7:1). */
.remove-confirm-source {
display: block;
margin: 0 0 0.75rem;
padding: 0.5rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
overflow-wrap: anywhere;
}
.remove-confirm-copy {
margin: 0;
color: var(--ink); /* 13.8:1 on --surface */
}
/* The in-modal failure line (role=alert): the err pair (err-ink on
err-bg 9.3:1, the err-line border) — the .git-source-error banner
language, boxed. */
.remove-confirm-error {
margin: 0.75rem 0 0;
padding: 0.5rem 0.65rem;
color: var(--err-ink);
background: var(--err-bg);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
}
.remove-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
margin-top: 1.1rem;
}
/* The two modal buttons: >=44px targets, the house 3px :focus-visible
via the global outline rule (no local override). */
.remove-confirm-btn {
min-height: 44px;
min-width: 44px;
padding: 0.55rem 1.1rem;
border-radius: var(--radius-sm);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
.remove-confirm-btn:disabled { opacity: 0.5; cursor: wait; }
/* Cancel — the ghost ink-soft family (5.1:1 on --surface), like the
row's Remove / .tuning-edit; the brand pair on hover (6.9:1). */
.remove-confirm-cancel {
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
}
.remove-confirm-cancel:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
/* "Remove source" — the destructive button on the err token family
(the .tuning-delete / .steering-delete convention): err-ink on
err-bg 9.3:1, the err-line border; the hover inverts to dark --bg
on --err-line (5.2:1 — AA). */
.remove-confirm-remove {
border: 1px solid var(--err-line);
background: var(--err-bg);
color: var(--err-ink);
}
.remove-confirm-remove:hover:not(:disabled) {
background: var(--err-line);
color: var(--bg);
}
/* ---------- Per-source ignore-paths editor (phase 89, task 05) ----------
The shell's Sources view: the page-local alertdialog that edits
one source's ignore list (one path per line — the phase-89 A1
prefix rule, stated in plain words in the helper copy). The EXACT
#remove-confirm-dialog overlay contract (phase 69): a fixed
full-viewport dim backdrop + a centered panel (z-index 1000, above
the sticky header (20) + skip-link (100); NO blur — the phase-08
no-blur perf anchor), scaled to the chat-column width (the
--chat-column token — 72rem, the .container width) or the
viewport, whichever is narrower. The box: a VISIBLE block label
(WCAG — never aria-label-only) over a mono textarea (the box is a
data entry, not prose — the var(--mono) stack); the error line is
the err pair (err-ink on err-bg 9.3:1, the err-line border);
Cancel is the ghost ink-soft family (5.1:1 on --surface); Save is
the solid brand family (--bg text on --brand 5.2:1 — the
.new-chat-btn convention; the hover lightens the fill). Both
buttons >=44px; :focus-visible via the global 3px outline rule (no
local suppression — focus lands on Cancel at open and is visible);
no animation (reduced-motion safe by construction). AA pairs: title
/ copy / label are --ink (or ink-soft) on --surface (13.8:1 /
5.1:1); the source value is --ink on --bg (16.7:1). Phase-08
tokens only; system fonts; no CDN. */
.ignore-editor {
position: fixed;
inset: 0;
z-index: 1000;
display: flex; /* the panel is the only in-flow child — margin: auto centers it */
}
/* Explicit (the global [hidden] rule already wins — the documented,
testable contract for the skeleton). */
.ignore-editor[hidden] { display: none; }
.ignore-editor-backdrop {
position: fixed;
inset: 0;
/* --bg at 82% — the doc-modal dim, no backdrop-filter (no-blur). */
background: color-mix(in srgb, var(--bg) 82%, transparent);
}
.ignore-editor-panel {
/* position:relative lifts the panel above the fixed backdrop
(positioned elements paint over in-flow siblings otherwise). */
position: relative;
z-index: 1;
margin: auto;
width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
}
.ignore-editor-title {
margin: 0 0 0.75rem;
font-size: 1.25rem;
line-height: 1.3;
color: var(--ink);
}
/* The source's value (git URL or local path) — mono, wrapped (a long
URL must not overflow the panel), --ink on --bg (16.7:1). */
.ignore-editor-source {
display: block;
margin: 0 0 0.75rem;
padding: 0.5rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
overflow-wrap: anywhere;
}
.ignore-editor-copy {
margin: 0 0 1rem;
color: var(--ink); /* 13.8:1 on --surface */
}
.ignore-editor-copy code { font-family: var(--mono); font-size: 0.85em; }
/* Visible block label (WCAG — the textarea is NEVER labelled only via
aria-label); ink-soft on --surface (5.1:1). */
.ignore-editor-label {
display: block;
margin-bottom: 0.35rem;
color: var(--ink-soft);
font-size: 0.88rem;
font-weight: 600;
}
/* The box: full panel width, mono (the var(--mono) stack), theme-
consistent border on --bg; the focus-visible ring is the global
3px outline (no local suppression); vertical resize for long
lists (up to 200 entries, A4). */
.ignore-editor-textarea {
display: block;
width: 100%;
min-height: 9rem;
padding: 0.55rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
resize: vertical;
}
/* Phase 121 (task 03): the editor's masked token field — the
textarea's box language (full panel width, --bg fill, the line
border, mono) at the 44px touch floor; the label's top margin keeps
the field pair off the textarea. type=password + autocomplete=off
live in the markup (a PAT is not a site credential — no browser
save offer); the focus ring is the global 3px outline rule. */
.ignore-editor-token-label { margin-top: 0.9rem; }
.ignore-editor-token {
display: block;
width: 100%;
min-height: 44px;
padding: 0.55rem 0.65rem;
font-family: var(--mono);
font-size: 0.85rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
}
/* The in-dialog failure line (role=alert): the err pair (err-ink on
err-bg 9.3:1, the err-line border) — the .remove-confirm-error
language. */
.ignore-editor-error {
margin: 0.75rem 0 0;
padding: 0.5rem 0.65rem;
color: var(--err-ink);
background: var(--err-bg);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
}
.ignore-editor-actions {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
margin-top: 1.1rem;
}
/* The two dialog buttons: >=44px targets, the house 3px :focus-visible
via the global outline rule (no local override). */
.ignore-editor-btn {
min-height: 44px;
min-width: 44px;
padding: 0.55rem 1.1rem;
border-radius: var(--radius-sm);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
.ignore-editor-btn:disabled { opacity: 0.5; cursor: wait; }
/* Cancel — the ghost ink-soft family (5.1:1 on --surface), like the
remove dialog's Cancel; the brand pair on hover (12.4:1 on
brand-soft). */
.ignore-editor-cancel {
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
}
.ignore-editor-cancel:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
/* "Save" — the solid brand family (the .new-chat-btn convention):
--bg text on --brand (5.2:1 — AA); the hover lightens the brand
fill (the .new-chat-btn hover pair). */
.ignore-editor-save {
border: 0;
background: var(--brand);
color: var(--bg);
}
.ignore-editor-save:hover:not(:disabled) { background: var(--brand-hover); }
/* The list: the Sources page's table pattern — full width in the
72rem frame, surface card, horizontally scrollable wrapper (the
URL column never wraps or ellipsizes: long URLs, credentials
included, scroll the wrapper instead of truncating). */
#git-sources-table-wrap {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow-x: auto;
}
.git-sources-table {
width: 100%;
border-collapse: collapse;
min-width: 560px;
font-size: 0.93rem;
}
.git-sources-table th, .git-sources-table td {
text-align: left;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
white-space: nowrap;
}
.git-sources-table th {
background: var(--brand-soft);
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
position: sticky;
top: 0;
}
/* Location cell: mono at the Sources-table size; the is plain
(no chip background — the cell IS the mono readout). Phase 38:
leads with the kind badge (Git/Local) — the kinds are told apart by
TEXT as much as color (WCAG 1.4.1), and both badge pairs are AA:
brand-ink on brand-soft 6.9:1 (Git), ok-ink on ok-bg 10.6:1
(Local). */
.git-sources-table td.git-source-url-cell { font-family: var(--mono); font-size: 0.82rem; }
.git-sources-table td.git-source-url-cell code { font-family: inherit; }
.git-source-kind {
display: inline-block;
margin-right: 0.55rem;
padding: 0.08rem 0.5rem;
border-radius: 999px;
font-family: var(--font);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
vertical-align: 0.06em;
white-space: nowrap;
}
.git-source-kind.is-git { color: var(--brand-ink); background: var(--brand-soft); }
.git-source-kind.is-local { color: var(--ok-ink); background: var(--ok-bg); }
.git-sources-table tbody tr:hover { background: var(--bg); }
.git-sources-table tbody tr:last-child td { border-bottom: 0; }
/* Per-row Remove: the tuning row-action language (icon + label,
>=44px) with the Delete hover pair (err 9.1:1). */
.git-source-remove {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
min-height: 44px;
min-width: 44px;
flex: 0 0 auto;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.git-source-remove svg { width: 14px; height: 14px; display: block; }
.git-source-remove:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.git-source-remove:disabled { opacity: 0.5; cursor: wait; }
/* Phase 89: the per-row "Ignore paths" trigger (makeRow — left of
Remove): the .git-source-remove idiom (same size/spacing, >=44px
target) in a NEUTRAL secondary fill — it must read distinct from
the destructive Remove (text label included, never icon-only; the
hover takes the brand-soft pair, 12.4:1 — Remove hovers to the err
pair, 9.3:1). */
.git-source-ignore {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
min-width: 44px;
flex: 0 0 auto;
padding: 0.35rem 0.7rem;
margin-right: 0.4rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.git-source-ignore:hover:not(:disabled) {
background: var(--brand-soft);
color: var(--brand-ink);
}
/* Phase 89: the "N ignored" count tag — next to the location
in the Source cell (TEXT + a distinct background, never color
alone — WCAG 1.4.1): --ink on --bg (16.7:1) inside the --surface
card; the line border keeps the chip legible when the row hover
swaps the cell onto --bg too. */
.git-source-ignore-count {
display: inline-block;
margin-left: 0.55rem;
padding: 0.08rem 0.5rem;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
color: var(--ink);
background: var(--bg);
white-space: nowrap;
}
/* Phase 105 (A5): the per-row "Hidden" checkbox — the actions
cell, LEFT of the "Ignore paths" button (JS builds it,
git-sources.js makeRow). A native labeled checkbox: the
visible "Hidden" text + the checkbox's own aria-label
(full source value). ~44px hit height matches the action
buttons; :focus-visible via the GLOBAL rule (L146) — no
per-control ring needed. */
.git-source-hidden {
display: inline-flex;
align-items: center;
gap: 0.4rem;
height: 44px;
padding: 0 0.5rem;
font-size: 0.8rem;
color: var(--ink);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
/* --brand checkbox fill: the native check renders in --bg on
--brand — VERIFIED (phase 105): built-in dark theme
#0f0a0a on #f43f5e = 5.2:1 (the house brand pairing;
measured 5.35:1 — WCAG AA ≥4.5:1); every theme-tab-saved
palette is gated by the tab's eight-pair contrast check
(theme.js PAIRS — "bg" on "brand" at AA_MIN 4.5), so the
checked-state ratio stays AA in all themes. */
.git-source-hidden input[type="checkbox"] {
width: 1.05rem;
height: 1.05rem;
margin: 0;
accent-color: var(--brand);
cursor: pointer;
}
.git-source-hidden:disabled { opacity: 0.5; cursor: wait; }
/* Phase 105 (A5): the "hidden on" state tag — a copy of the
.git-source-ignore-count idiom (TEXT + background, never
color alone — WCAG 1.4.1: --ink on --bg 16.7:1). */
.git-source-hidden-count {
display: inline-block;
margin-left: 0.55rem;
padding: 0.08rem 0.5rem;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
color: var(--ink);
background: var(--bg);
white-space: nowrap;
}
/* Env-fallback rows carry no Remove (nothing is stored to remove) —
the tag says where the row comes from (brand pair, 6.9:1). */
.git-source-env-tag {
color: var(--brand-ink);
background: var(--brand-soft);
border-radius: 999px;
padding: 0.2rem 0.6rem;
font-size: 0.78rem;
font-weight: 600;
}
/* Empty state: the tuning page's quiet centered line at full table
width — no stored rows AND no env fallback to show. */
.git-sources-empty {
margin: 0;
padding: 1.4rem 1rem;
text-align: center;
color: var(--ink-soft);
font-style: italic;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
/* ---------- History refresh button (phase 77 task 03, TODO.md L3) ----------
The #view-history page-head is a flex row: the title block left, the
actions slot right (the row wraps below 640px). Scoped to
#view-history — the other four views' page-heads are untouched.
The button reuses the .new-chat-btn visual language: solid brand
pill, --bg text on --brand (5.2:1, WCAG AA >=4.5:1), borderless,
>=44px target, hover lightens the brand fill, focus-visible via the
global 3px rule. The phase-46 auth-link convention: the label is
visible >=640px, the glyph is the whole control below (the button's
aria-label keeps the accessible name in both). */
#view-history .page-head {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem 1.5rem;
}
#view-history .page-head .page-head-title { min-width: 0; }
.history-refresh {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 0;
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 700;
font-size: 0.95rem;
white-space: nowrap;
cursor: pointer;
flex-shrink: 0;
}
.history-refresh:hover { background: var(--brand-hover); color: var(--bg); }
.history-refresh:disabled { opacity: 0.6; cursor: wait; }
/* The refresh glyph is hidden on desktop (the label carries the
pill); below 640px it joins the visible label in the full-width
pill (the <=640 block — the icon-only squeeze is gone). */
.history-refresh svg { width: 16px; height: 16px; display: none; }
/* ---------- History page (phase 50) ----------
/history.html: the admin-only saved-chats list (task 04). The
FULL-WIDTH table in the 72rem frame (AGENTS.md rule 5 — no skinny
single-column list), the same table language as the Sources page
(the sources-table family: --line hairlines, the brand-soft-tinted
thead, row hover, the scrollable .table-wrap). Every pair reuses
the Phase-08 AA palette: brand-ink on brand-soft 6.9:1, ink-soft
>=6.9:1, err 9.1:1. :focus-visible via the global 3px outline
rule. No CDN, system fonts. */
.history-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* Action feedback line (role=status): the sync-result shape —
ink-soft on surface (>=4.5:1), small mono; the min-height holds
the layout so a delete's line never reflows the table. */
.history-status {
display: block;
min-height: 1.2em;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
}
/* The table is FULL-WIDTH (AGENTS.md rule 5): width 100% inside the
standard .container; the .table-wrap card + its horizontal scroll
cover narrow widths (the phase-07 responsive contract). */
.history-table {
width: 100%;
border-collapse: collapse;
min-width: 640px;
font-size: 0.93rem;
}
.history-table th, .history-table td {
text-align: left;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.history-table th {
background: var(--brand-soft);
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.history-table tbody tr:hover { background: var(--bg); }
.history-table tbody tr:last-child td { border-bottom: 0; }
/* Title cell: the Open link — the accent link (brand-ink on surface
6.9:1); the column ellipsizes, the full title sits in the title
attribute (on the cell AND the link). */
.history-title-cell {
max-width: 34rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-title-link {
color: var(--brand-ink);
font-weight: 600;
text-decoration: none;
}
.history-title-link:hover { text-decoration: underline; }
.history-title-link:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Messages: the mono numeric readout (ink-soft >=6.9:1). */
.history-count-cell { font-family: var(--mono); color: var(--ink-soft); }
/* Updated: locale date+time (ink-soft), the full ISO in the title
attribute (history.js). */
.history-updated-cell { color: var(--ink-soft); white-space: nowrap; }
/* Stale marker (phase 53, task 04): the READ-ONLY staleness badge on
rows saved before the last KB-changing sync (the Regenerate action
lives on the chat-page banner — the marker is a pill, never a
control). The rose family, i.e. the Stop-treatment tokens, so
"stale" reads in the same visual language as the in-flight control:
err-ink on err-bg ≈9.3:1 (≥4.5:1 on --surface too), err-line
border — theme tokens, AA in the palette as a whole. Fresh rows'
em-dash rides the cell's ink-soft (5.1:1 on --surface). */
.history-stale-cell { color: var(--ink-soft); white-space: nowrap; }
.stale-pill {
display: inline-block;
padding: 0.15rem 0.55rem;
border: 1px solid var(--err-line);
border-radius: 999px;
background: var(--err-bg);
color: var(--err-ink);
font-size: 0.75rem;
font-weight: 700;
line-height: 1.45;
white-space: nowrap;
}
/* Actions: the Delete ghost button (the tuning row-action language)
+ the inline two-step confirm pair (phase 50 task 04). */
.history-actions { display: inline-flex; align-items: center; gap: 0.4rem; }
.history-delete {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-delete:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.history-delete:disabled { opacity: 0.5; cursor: wait; }
.history-confirm-text { color: var(--err-ink); font-size: 0.82rem; font-weight: 700; white-space: nowrap; }
/* Yes: the error-rose treatment (err-ink on err-bg 9.1:1, err-line
border); No: the ghost (transparent, --line border). */
.history-confirm-yes {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
background: var(--err-bg);
color: var(--err-ink);
font: inherit;
font-weight: 700;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-confirm-yes:hover:not(:disabled) { background: color-mix(in srgb, var(--err-line) 18%, transparent); }
.history-confirm-yes:disabled { opacity: 0.6; cursor: wait; }
.history-confirm-no {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-confirm-no:hover { background: var(--brand-soft); color: var(--brand-ink); }
/* Share column (phase 51, owner-locked 2026-08-29): the Tune/Retry-
family ghost buttons — Create link (unshared) and Copy (shared);
Unshare is the .history-unshare ghost and its two-step confirm reuses
the .history-confirm-* pair CSS above (the phase-50 pattern). The
row-action language of .history-delete: --line border, transparent
fill, ink-soft (>=5.1:1), ≥44px comfortable target, focus-visible
via the global 3px rule; hover takes the brand pair (6.9:1). */
.history-share { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: nowrap; }
.history-share-cell { white-space: nowrap; }
.history-share-create,
.history-share-copy,
.history-unshare {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.history-share-create:hover:not(:disabled),
.history-share-copy:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); }
.history-unshare:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.history-share-create:disabled,
.history-share-copy:disabled,
.history-unshare:disabled { opacity: 0.5; cursor: wait; }
/* The share link's inline fallback field (phase 51, owner-locked): a
non-secure (http) origin rejects the clipboard, so the full URL is
offered as an input-like that selects itself on focus — click or
Tab, then Ctrl/Cmd+C. Mono (the URL is data), surface fill, --line
border; truncates with an ellipsis at narrow widths (the full URL is
the title + the text selection). Ink on surface ≈13.8:1. */
.share-link-fallback {
display: inline-block;
max-width: 14rem;
padding: 0.35rem 0.55rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font-family: var(--mono);
font-size: 0.78rem;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.share-link-fallback:hover { border-color: var(--brand); }
.share-link-fallback:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Empty-state row: the muted centered message at full table width
(the .git-sources-empty language, inline in the table). */
.history-empty-row td {
padding: 2.25rem 1rem;
text-align: center;
color: var(--ink-soft);
font-style: italic;
}
/* ---------- Tokens view (phase 79 task 06) ----------
The admin-issued access tokens (generate · list · revoke). The
shell's standard frame (the .container): the page-head (h1 + sub),
the #tokens-gate (the #history-gate / .sources-gate language), the
create row (label + Generate), the shown-once block (the mono
field + Copy), and the FULL-WIDTH table (AGENTS.md rule 5 — the
.history-table language: --line hairlines, the brand-soft-tinted
thead, row hover, the scrollable .table-wrap). Every pair reuses
the Phase-08 AA palette: brand-ink on brand-soft 6.9:1, ink-soft
>=5.1:1, err 9.3:1. :focus-visible via the global 3px outline
rule. No CDN, system fonts. */
.tokens-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* Action feedback line (role=status): the .history-status shape —
the min-height holds the layout so a line never reflows the table. */
.tokens-status {
display: block;
min-height: 1.2em;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
}
/* The create row: label input + Generate — flex, wraps below 640px
(the button drops under the full-width input). */
.token-create {
display: flex;
align-items: center;
gap: 0.6rem;
}
#token-label {
flex: 1;
min-width: 0;
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font: inherit;
font-size: 0.93rem;
}
#token-label:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Generate: the .history-refresh brand pill language — the solid
brand fill (--bg text on --brand 5.2:1, AA), the ≥44px target, the
lightened hover fill, the dimmed :disabled (the in-flight state),
the global :focus-visible ring. */
.token-generate {
min-height: 44px;
padding: 0.4rem 1.1rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
.token-generate:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); }
.token-generate:disabled { opacity: 0.6; cursor: wait; }
/* The shown-once block (owner-locked A4): a quiet brand-soft card
around the "shown once" line + the mono read-only field + Copy —
the token reads as the app's own credential surface (brand-ink on
brand-soft 12.4:1; ink on the field 13.8:1). */
.token-once {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.9rem 1rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--brand-soft);
}
.token-once-copy {
margin: 0;
color: var(--brand-ink);
font-size: 0.85rem;
font-weight: 600;
}
.token-once-row {
display: flex;
align-items: center;
gap: 0.6rem;
}
/* The mono read-only field: the token is DATA — mono, surface fill,
--line border; it truncates with an ellipsis at narrow widths (the
full value is the text selection — the inline copy fallback). */
#token-once-value {
flex: 1;
min-width: 0;
min-height: 44px;
padding: 0.35rem 0.55rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font-family: var(--mono);
font-size: 0.78rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#token-once-value:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
/* Copy: the Tune/Retry-family ghost button (the .history-share-copy
language — --line border, transparent fill, ink-soft, ≥44px, the
brand hover pair). */
#token-once-copy {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
#token-once-copy:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
/* The table is FULL-WIDTH (AGENTS.md rule 5): width 100% inside the
standard .container; the .table-wrap card + its horizontal scroll
cover narrow widths (the phase-07 responsive contract). The
.history-table language, verbatim. */
.tokens-table {
width: 100%;
border-collapse: collapse;
min-width: 640px;
font-size: 0.93rem;
}
.tokens-table th, .tokens-table td {
text-align: left;
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.tokens-table th {
background: var(--brand-soft);
color: var(--brand-ink);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.tokens-table tbody tr:hover { background: var(--bg); }
.tokens-table tbody tr:last-child td { border-bottom: 0; }
/* Label: the hand-out name (the column ellipsizes, the full label
sits in the title attribute — tokens.js). */
.tokens-label-cell {
max-width: 20rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
}
/* Created / Last used: locale date+time (ink-soft), the full ISO in
the title attribute; "never" for the not-yet-used stamp. */
.tokens-date-cell { color: var(--ink-soft); white-space: nowrap; }
/* Status: the Active em-dash rides the cell's ink-soft (5.1:1 on
--surface); the Revoked marker reuses the .stale-pill rose family
(err-ink on err-bg ≈9.3:1, err-line border — the stale-pill visual
language). */
.tokens-status-cell { color: var(--ink-soft); white-space: nowrap; }
/* Actions: the Revoke ghost button (the .history-delete language —
--line border, transparent fill, ink-soft, ≥44px target) + the
inline two-step confirm (the .history-confirm-* pair CSS above —
the phase-50 pattern). */
.tokens-actions { display: inline-flex; align-items: center; gap: 0.4rem; }
.token-revoke {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.token-revoke:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.token-revoke:disabled { opacity: 0.5; cursor: wait; }
/* Regenerate (phase 101 task 03, D2): the .token-revoke's structural
twin — the SAME size / border / radius / focus, the transparent
fill + ink-soft text — but the NEUTRAL action's hover
(brand-soft / brand-ink), NOT the revoke's error hover: a
rotation is a hand-out of a successor, not a deletion (no new
hue — the phase-92 monochrome invariant). The confirm pair
reuses the .history-confirm-* classes unchanged (the pair CSS
above). :disabled mirrors the revoke button's rule (the
in-flight state, while the rotation POST is out). */
.token-regenerate {
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.82rem;
white-space: nowrap;
cursor: pointer;
}
.token-regenerate:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); }
.token-regenerate:disabled { opacity: 0.5; cursor: wait; }
/* Empty-state + search no-match rows (phase 101): the muted centered
message at full table width (the .history-empty-row language,
inline in the table) — CLASS-based, so both the active table's
#tokens-empty-row / #tokens-no-match-row and the revoked table's
#tokens-revoked-no-match-row share it. */
.tokens-empty-row td {
padding: 2.25rem 1rem;
text-align: center;
color: var(--ink-soft);
font-style: italic;
}
/* Phase 101 (task 02, D4): the per-table live label search — the
house input surface (the #token-label / archive-upload input
family: --line hairline, --surface fill, ink text — AA pairs, no
new hue), full width, the ≥44px target, the global :focus-visible
ring (no custom focus rule — the house pattern). */
.token-search {
width: 100%;
min-height: 44px;
padding: 0.55rem 0.8rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font: inherit;
font-size: 0.9rem;
}
/* The revoked section's sub-heading (phase 101 D1): the phase-97
.kb-level h2 voice — mono, 1rem, brand-ink (the path voice, AA on
the page background). */
.tokens-revoked-heading {
margin: 1.5rem 0 0.5rem;
font-family: var(--mono);
font-size: 1rem;
color: var(--brand-ink);
}
/* ---------- Theme view (phase 91, tasks 04 + 05) ----------
The shell's seventh view (#view-theme): the admin palette + branding
editor (task 05). The full 72rem container width (owner
instruction 2026-09-12 — the phase-91 form-column cap is
superseded), the
form card (the #tune-form language: surface fill, --line hairline,
radius, shadow), the fieldset groups (Branding / Palette) with the
.theme-note sub-copy, the 3-column (desktop) / 1-column (mobile)
palette grid with the color swatch inputs sized for touch (44px —
the label above is the second tap affordance), and the Save/Reset
row (Save = the brand pill family, --bg ink on --brand 5.2:1 AA;
Reset = the ghost button family, --line border; the §7.4
disabled-while-in-flight look is the :disabled pair below). The
feedback lines: error/result are the house role=status/alert line
languages; #theme-contrast (the WCAG warning) uses the --err-*
STATE family — states are semantic colors, never themed from the
tab (B3). Every pair reuses the Phase-08 AA palette; :focus-visible
via the global 3px outline rule. No CDN, system fonts. */
.theme-shell {
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* The static form — one card holding the two fieldset groups + the
actions row (the #tune-form card language). */
#theme-form {
display: flex;
flex-direction: column;
gap: 1rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1rem 1.1rem 1.1rem;
}
/* The fieldset groups: reset the UA border (the card IS the group's
frame), the legend rides the group's title line (the
.tuning-panel-title language — ink, 1rem, 700). */
.theme-group {
margin: 0;
padding: 0;
border: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.theme-group-title {
padding: 0;
font-size: 1rem;
font-weight: 700;
color: var(--ink);
}
/* The branding text inputs: the #token-label language (surface is
already the card fill — transparent field, --line hairline, ≥44px
target, the global :focus-visible ring). */
#theme-form fieldset label {
font-size: 0.88rem;
font-weight: 600;
color: var(--ink-soft);
}
#theme-form fieldset input[type="text"] {
width: 100%;
min-height: 44px;
padding: 0.35rem 0.7rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink);
font: inherit;
font-size: 0.93rem;
}
/* The palette note under the branding legend (task 05): the strings
apply on the next page load (B4) — the live preview covers the
palette only. */
.theme-note {
margin: 0;
font-size: 0.8rem;
line-height: 1.35;
color: var(--ink-soft);
}
/* The color-input grid (task 05): the 8 swatches in 3 columns
(3 + 3 + 2 rows) — the cell is a .theme-color (label above the
swatch); 1 column at the mobile breakpoint (below). */
.theme-colors {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.6rem 0.9rem;
}
.theme-color {
display: flex;
flex-direction: column;
gap: 0.3rem;
min-width: 0;
}
.theme-color label {
font-size: 0.8rem;
font-weight: 600;
color: var(--ink-soft);
line-height: 1.25;
}
.theme-color input[type="color"] {
/* Touch-sized swatch (task 05): 44px tall (the WCAG 2.5.8 target —
the label above is the second tap affordance). */
inline-size: 56px;
block-size: 44px;
padding: 3px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg);
cursor: pointer;
}
/* Save (primary) + Reset (secondary): the pill + ghost families
(.history-refresh / .token-revoke languages — the global
:focus-visible ring applies, no button-scoped focus override). */
.theme-actions {
display: flex;
gap: 0.6rem;
margin-top: 0.2rem;
}
#theme-save {
min-height: 44px;
padding: 0.4rem 1.2rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 (never white on brand) */
font: inherit;
font-weight: 700;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
#theme-save:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); }
#theme-save:disabled { opacity: 0.6; cursor: wait; }
.theme-reset {
min-height: 44px;
padding: 0.4rem 1rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
cursor: pointer;
}
.theme-reset:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
.theme-reset:disabled { opacity: 0.6; cursor: wait; }
/* The three feedback lines (task 05): error (role=alert — the
server's 422 detail) and result (role=status) are the house line
languages; min-height holds the layout so a line never reflows the
form. #theme-contrast (the WCAG warning, task 05) takes the
.tuning-error box treatment in the --err-* STATE family — states
are semantic colors, never themed from the tab (B3): a failing
pair reads as a warning and the box is warning-only (Save is never
disabled by it). */
.theme-error,
.theme-result,
.theme-contrast {
display: block;
margin: 0;
min-height: 1.2em;
font-family: var(--mono);
font-size: 0.8rem;
padding-block: 0.25rem;
}
.theme-error { color: var(--err-ink); }
.theme-result { color: var(--ok-ink); }
.theme-contrast {
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.8rem;
}
/* ---------- Shared page (phase 51, task 03) ----------
/shared/: the anonymous read-only conversation (owner-locked
2026-08-29, TODO.md L6). The shared page reads exactly like the
chat page — both at the 72rem container width (--chat-column, now
equal to the .container cap — owner instruction 2026-09-12; the
wide-desktop doubling was retired) — and the .msg/.bubble/.thinking/
.tool-calls/.msg-meta rules apply unchanged, with NO composer, so
the column contract holds for a guest. Zero interactive controls
(owner-locked): the chips are
plain text, so the pill families' pointer treatments are switched
off IN THIS SCOPE ONLY — the chat page's interactive chips keep
their styles untouched. Every pair reuses the Phase-08 AA palette
(ink-soft ≥6.9:1 on surface/bg, brand-ink on brand-soft 6.9:1);
:focus-visible via the global 3px outline rule. No CDN, system
fonts. */
.shared-shell {
width: 100%;
max-width: var(--chat-column); /* the centered chat column — 72rem,
the .container width at every viewport */
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
}
/* Page title (JS-filled with the shared chat's title; the static
fallback is "Shared conversation") — the page-head h1 size. */
#shared-title { margin: 0 0 0.25rem; font-size: 1.7rem; }
/* The muted meta line under the h1 ("Shared via … — read-only."):
ink-soft on the page bg ≥8.6:1, the page-sub language. */
.shared-note {
margin: 0;
color: var(--ink-soft);
font-size: 0.92rem;
}
/* The invalid / revoked state: a centered muted card (the not-found
language — surface fill, --line border, italic ink-soft). Revealed
by shared.js for a malformed token (no fetch) or a failed read. */
#shared-invalid {
padding: 1.4rem 1rem;
text-align: center;
color: var(--ink-soft);
font-style: italic;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
/* Static chips: a guest's "Maybe try" and source chips are TEXT — no
pointer, no hover, no cursor (owner-locked zero controls). The pill
look is kept; the interactivity is scoped off here and only here
(pointer-events: none also makes the :hover rules unreachable). */
.shared-shell .suggestion-chip,
.shared-shell .source-chip {
pointer-events: none;
cursor: default;
}
/* ---------- Document viewer (phase 10; two-row header since phase 34) ---------- */
/* Phase 34 (owner confirmation 2026-08-26): the viewer header is TWO
rows in one sticky — row 1 reuses the standard .app-header /
.header-inner rules VERBATIM (row 1 IS the standard bar, so the
phase-12/19 pinned heights 64px / 58px apply), row 2 is the
.doc-titlebar (back link + title + meta). The header element itself
is content-sized (row 1 + row 2) and sticky, so BOTH rows stay
pinned while the document scrolls. */
.doc-header {
position: sticky;
top: 0;
z-index: 20;
background: var(--surface);
/* Phase 34: no fixed height — the header sizes to row 1 (--header-h)
+ the .doc-titlebar; the phase-12 shrink guard stays. */
flex-shrink: 0;
}
/* Row 1's own gradient hairline would land BETWEEN the rows; the
separator there is the quiet --line border-top on the titlebar, and
the signature hairline belongs at the bottom edge of the whole
header (the .doc-header::after in the shared rule above). */
.doc-header > .app-header::after { content: none; }
/* Back link: pill with an SVG arrow + "Sources" (>=44px touch target;
:focus-visible via the global 3px outline rule, phase 08). */
.doc-back {
display: inline-flex;
align-items: center;
gap: 0.4rem;
flex: 0 0 auto;
min-height: 44px;
padding: 0.45rem 0.9rem;
border-radius: 999px;
background: var(--brand-soft);
border: 1px solid var(--line);
color: var(--brand-ink); /* 6.9:1 on --brand-soft */
font-weight: 600;
font-size: 0.95rem;
text-decoration: none;
}
.doc-back:hover { background: var(--brand-soft); }
.doc-back svg { width: 16px; height: 16px; display: block; }
/* Row 2: the titlebar — a .container-width row with the back link +
the title block, its own content-sized height (title line + meta
line), the same surface as row 1, separated from it by the quiet
hairline (var(--line) — the existing 1px border color). */
.doc-titlebar {
border-top: 1px solid var(--line);
padding-block: 0.6rem;
}
.doc-titlebar .container {
display: flex;
align-items: center;
gap: 0.9rem;
}
/* The title block keeps clipping (min-width: 0) so #doc-title /
.doc-meta ellipsize inside the flex row instead of overflowing. */
.doc-title-block { min-width: 0; }
#doc-title {
margin: 0;
font-size: 1.3rem;
line-height: 1.3;
/* Phase 34: ellipsis + the `title` attribute (set by document.js) —
the row has no pills to fit, so the old clipping-for-pills rule is
gone; the ellipsis is the only fit the title needs. */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Meta row: source badge · format badge · mono path · created ·
indexed · chunks (phase 106 (task 08, D8) added created BEFORE
indexed). Phase 12/34: it may clip, but it must NEVER wrap — the
meta stays on one line in the titlebar row (the row is content-
sized, so a wrap would grow it). */
.doc-meta {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.4rem;
margin-top: 0.35rem;
font-size: 0.78rem;
color: var(--ink-soft);
/* Overflowing content (badges + path + dates) clips at the bar edge;
nowrap keeps text on one line so the row can never grow the header. */
white-space: nowrap;
overflow: hidden;
}
.doc-source-badge {
background: var(--brand-soft);
border: 1px solid var(--line);
color: var(--brand-ink);
font-weight: 600;
border-radius: 999px;
padding: 0.02rem 0.55rem;
}
.format-badge {
font-family: var(--mono);
background: var(--brand-soft);
border: 1px solid var(--line);
color: var(--brand-ink);
border-radius: var(--radius-sm);
padding: 0.02rem 0.45rem;
}
.doc-path {
font-family: var(--mono);
font-size: 0.75rem;
max-width: 26rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* The path is the meta row's designated ellipsis target: with a floor it
keeps a visible box on narrow screens while the trailing badges clip. */
min-width: 6rem;
}
.doc-chunks { font-family: var(--mono); }
/* Phase 106 (task 08, D8): the Created badge — the SAME family as the
Indexed one (no own look: the meta row's text, var(--ink-soft) on
the titlebar's --surface — 5.1:1, the pair recorded at :root; the
modal's .doc-modal-meta row records the same pair). The explicit
declaration pins the inherited value: a themed --ink-soft moves the
badge with the row (byte-identical rendering). */
.doc-created { color: var(--ink-soft); /* 5.1:1 on --surface */ }
.doc-shell {
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
}
#doc-content { display: flex; flex-direction: column; }
.doc-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); }
/* Markdown: the centered reading column — 72rem, the .container width
at every viewport (owner instruction 2026-09-12: "match the width
of the RAG page for all other pages" — the wide-desktop doubling
was retired). */
.doc-md {
width: 100%;
max-width: var(--chat-column);
margin-inline: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.5rem 1.75rem;
overflow-wrap: anywhere;
}
.doc-md p { margin: 0.35rem 0; }
.doc-md h3 { margin: 1.1rem 0 0.4rem; font-size: 1.15rem; }
.doc-md h4 { margin: 0.9rem 0 0.35rem; font-size: 1rem; }
.doc-md > :first-child { margin-top: 0; }
.doc-md ul { margin: 0.4rem 0; padding-left: 1.3rem; }
.doc-md pre {
background: var(--surface);
color: var(--ink);
padding: 0.7rem 0.9rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
overflow-x: auto;
font-size: 0.85rem;
font-family: var(--mono);
}
.doc-md code { font-family: var(--mono); font-size: 0.88em; background: var(--brand-soft); padding: 0.08em 0.35em; border-radius: 5px; }
.doc-md pre code { background: none; padding: 0; }
/* Summary panel (phase 36): the labeled "Summary" section the shared
renderDocument core (document.js) draws ABOVE the original content
whenever doc.summary is non-empty (phase 30 — non-markdown docs
only). A "summary, not content" look: the surface card carries a 3px
brand left border (no shadow — the content cards own those) and a
small-caps brand-ink label. Phase-08 tokens only; static content —
no animation (nothing for prefers-reduced-motion to still), and the
aria-label + heading carry the accessibility. */
.doc-summary {
width: 100%;
background: var(--surface);
border: 1px solid var(--line);
border-left: 3px solid var(--brand);
border-radius: var(--radius);
padding: 0.9rem 1.1rem;
margin-bottom: 1rem;
overflow-wrap: anywhere;
}
/* md/markdown: the panel matches the .doc-md centered reading column
(72rem — the same --chat-column token, the .container width at
every viewport) — it is the column's label. Raw formats stay full width (the
.doc-raw default above), matching the full-width pre; in engines
without :has() the panel degrades to that full-width default. */
.doc-summary:has(+ .doc-md) {
max-width: var(--chat-column);
margin-inline: auto;
}
.doc-summary-title {
margin: 0 0 0.4rem;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brand-ink); /* 9.0:1 on --surface */
}
.doc-summary-text {
margin: 0;
color: var(--ink); /* on --surface ≈14.5:1 */
}
/* Summary edit affordance (phase 57, task 02 — D4, admin-only): the
header row (label + Edit button), the inline editor (prefilled
textarea + Save/Cancel + a role=status live region). House
dark-tech palette (phase-08 tokens), system fonts, no CDN;
:focus-visible via the global 3px outline rule. Anonymous visitors
never see any of it — the button and editor are wired only for
admins (docAdminReady in document.js), so the public panel is
byte-for-byte the phase-36 shape. */
.doc-summary-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.4rem; /* the old .doc-summary-title bottom margin */
}
.doc-summary-head .doc-summary-title { margin: 0; }
.doc-summary-edit {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
letter-spacing: 0.02em;
cursor: pointer;
}
.doc-summary-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
.doc-summary-edit[hidden] { display: none; } /* the hidden attr must beat the display above */
.doc-summary-editor {
display: block;
width: 100%;
min-height: 8rem;
padding: 0.6rem 0.8rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg); /* inset against the --surface panel */
color: var(--ink); /* 16.7:1 on --bg (AA) */
font: inherit;
line-height: 1.5;
resize: vertical;
}
.doc-summary-actions {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.75rem;
}
.doc-summary-save {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0.35rem 0.95rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */
font: inherit;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
}
.doc-summary-save:hover { background: var(--brand-hover); } /* the house hover lightening */
.doc-summary-save:disabled { opacity: 0.6; cursor: default; } /* one PATCH at a time */
.doc-summary-cancel {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0.35rem 0.95rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
}
.doc-summary-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.doc-summary-status {
margin: 0.6rem 0 0;
font-size: 0.85rem;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
}
.doc-summary-status:empty { margin-top: 0; }
/* Date edit affordance (phase 106, task 09 — D7, admin-only): the
"Edit date" text button after the Created badge (the
.doc-summary-edit family — one pill style for both inline
editors) and the editor box that swaps in for the button IN the
.doc-meta row: the native type=date input, Save / Cancel, the
muted underlined "Revert to sync" clear affordance (the phase-57
"clear = explicit" contrast), a role=status live line, and a
role=alert error line (the .git-source-error err pair). The meta
row may clip (nowrap) on the page, so the box wraps ITS children
internally (the modal's .doc-modal-meta already wraps). The
global 3px :focus-visible outline rule applies — no per-control
rule (the phase-105 checkbox idiom). :disabled = opacity +
cursor: wait (the .git-source-remove:disabled idiom — one PATCH
at a time). Contrast (verified, house style): button / Cancel /
revert / status ride the row's ink-soft on --surface (5.1:1, AA);
the input is ink on --bg (16.7:1, AA); Save is the solid brand
pill (--bg on --brand = 5.2:1, AA); the error line is the err
pair (9.1:1 on --err-bg, AA). Anonymous / token holders never see
any of it — the wiring is admin-gated (docAdminReady in
document.js), so the public badge row stays byte-for-byte the
task-08 shape. */
.doc-date-edit {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
letter-spacing: 0.02em;
cursor: pointer;
}
.doc-date-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
.doc-date-edit[hidden] { display: none; } /* the hidden attr must beat the display above */
.doc-date-editor {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
min-width: 0;
max-width: 100%;
}
.doc-date-input {
padding: 0.15rem 0.4rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg);
color: var(--ink); /* 16.7:1 on --bg (AA) */
font: inherit;
font-size: 0.78rem;
font-family: var(--mono);
}
.doc-date-save {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 0;
border-radius: 999px;
background: var(--brand);
color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
cursor: pointer;
}
.doc-date-save:hover { background: var(--brand-hover); } /* the house hover lightening */
.doc-date-cancel {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.78rem;
cursor: pointer;
}
.doc-date-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.doc-date-revert {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.15rem 0.2rem;
border: 0;
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) — the muted marker */
font: inherit;
font-size: 0.78rem;
text-decoration: underline;
cursor: pointer;
}
.doc-date-revert:hover { color: var(--ink); } /* 14.5:1 on --surface (AA) */
.doc-date-save:disabled,
.doc-date-cancel:disabled,
.doc-date-revert:disabled,
.doc-date-input:disabled {
opacity: 0.5;
cursor: wait; /* one PATCH at a time (the .git-source-remove:disabled idiom) */
}
.doc-date-status {
font-size: 0.78rem;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
}
.doc-date-error {
flex-basis: 100%; /* drops onto its own row under the controls */
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere; /* a long server detail never overflows the box */
font-size: 0.78rem;
font-weight: 600;
background: var(--err-bg);
color: var(--err-ink); /* 9.1:1 on --err-bg (AA — the .git-source-error pair) */
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.1rem 0.5rem;
}
.doc-date-status:empty, .doc-date-error:empty { display: none; } /* an empty live line takes no space */
/* Raw (non-markdown) formats: full-width mono pre, horizontal scroll. */
.doc-raw {
width: 100%;
margin: 0;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.25rem 1.5rem;
overflow-x: auto;
white-space: pre;
font-family: var(--mono);
font-size: 0.85rem;
line-height: 1.5;
}
/* 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 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. */
.doc-image-unavailable {
margin: 1.25rem 1rem;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font-size: 0.9rem;
}
/* Designed not-found state (no emoji — plain SVG mark, phase 08 rule). */
.doc-not-found {
width: 100%;
max-width: 30rem;
margin: 2rem auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2.25rem 1.75rem;
text-align: center;
}
.doc-not-found-glyph { color: var(--ink-soft); width: 44px; height: 44px; margin-inline: auto; }
.doc-not-found-glyph svg { width: 44px; height: 44px; display: block; }
.doc-not-found h2 { margin: 0.8rem 0 0.4rem; font-size: 1.3rem; }
.doc-not-found-sub { margin: 0 0 1.25rem; color: var(--ink-soft); }
.doc-open-sources {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0.5rem 1.1rem;
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 */
font-weight: 700;
text-decoration: none;
border-radius: var(--radius-sm);
}
.doc-open-sources:hover { background: var(--brand-hover); }
/* Viewer links: Sources-table path cell + chat source chips (phase 10). */
.doc-link {
color: var(--brand-ink); /* 8.7:1 on --surface */
text-decoration: none;
}
.doc-link:hover, .doc-link:focus-visible { text-decoration: underline; }
/* ---------- Document modal (phase 26) ----------
"New documents should open in an almost-fullscreen modal, not in a new
page" (TODO.md L4). The overlay reuses the viewer page's .doc-meta
badge classes, the .doc-md reading column (72rem — the .container
width; inside the modal the 1100px panel stays its effective
ceiling), and the .doc-raw
pre — this block only adds the chrome (backdrop, panel, header,
actions, scroll container). Phase-08 tokens only; NO blur (the
phase-08 no-blur perf anchor); no new assets; system fonts.
Stacking: z-index 1000 puts the overlay above the sticky header (20)
and the skip-link (100); the z-index:-1 background layers stay below
everything. The panel is 96vw × 92vh, centered ("almost-fullscreen"). */
.doc-modal {
position: fixed;
inset: 0;
z-index: 1000;
display: flex; /* the panel is the only in-flow child — margin: auto centers it */
}
/* Explicit (the global [hidden] rule already wins — this one is the
documented, testable contract for the skeleton). */
.doc-modal[hidden] { display: none; }
.doc-modal-backdrop {
position: fixed;
inset: 0;
/* --bg at 82% — no backdrop-filter (phase-08 no-blur perf anchor). */
background: color-mix(in srgb, var(--bg) 82%, transparent);
transition: opacity 120ms ease;
}
.doc-modal-panel {
/* position:relative lifts the panel above the fixed backdrop (positioned
elements paint over in-flow siblings otherwise). */
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(1100px, 96vw);
height: 92vh;
margin: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 12px;
/* Depth shadow — deliberately NOT themed (black, phase-92 table:
"depth, not theme"); the color-mix spelling renders the same 55%
black and keeps the literal out of the phase-92 sweep gate. */
box-shadow: 0 24px 80px color-mix(in srgb, black 55%, transparent);
}
/* Sticky top with the SAME height as the page bars — the phase-12 pins
(--header-h: 64px desktop / 58px ≤640px) so the bar never reads
differently here. The title is the designated squeeze target (ellipsis),
so the bar can never grow its height. */
.doc-modal-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.9rem;
height: var(--header-h);
padding-inline: 1.25rem;
background: var(--surface);
border-bottom: 1px solid var(--line);
}
.doc-modal-title {
min-width: 0;
margin: 0;
font-size: 1.3rem;
line-height: 1.3;
color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.doc-modal-actions {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* "Full page" escape hatch: the same ghost pill as New chat / Sign in
(ink-soft on surface ≈6.9:1; hover pair brand-ink/brand-soft ≈6.9:1).
Icon-only below 640px — the aria-label keeps the accessible name. */
.doc-modal-open {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
padding: 0.4rem 0.8rem;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.85rem;
white-space: nowrap;
text-decoration: none;
}
.doc-modal-open:hover { background: var(--brand-soft); color: var(--brand-ink); }
.doc-modal-open svg { width: 15px; height: 15px; display: none; }
/* Icon-only close (aria-label in the markup). ink-soft on surface ≈6.9:1;
hover = the err pair ≈9.1:1, like the steering-note delete. */
.doc-modal-close {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
padding: 0;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft);
cursor: pointer;
}
.doc-modal-close:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
.doc-modal-close:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 2px;
}
.doc-modal-close svg { width: 18px; height: 18px; display: block; }
/* Meta row: the SAME badge classes as the viewer's .doc-meta (source
badge · format badge · mono path · created · indexed · chunks — no
duplicate badge styling here); unlike the fixed-height header bar it
may WRAP, so nothing clips. aria-live="polite" on the element
announces the load → meta swap (phase-10 a11y contract, modal
variant). */
.doc-modal-meta {
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
padding: 0.6rem 1.25rem 0;
font-size: 0.78rem;
color: var(--ink-soft);
}
/* The scroll container: vertical scroll lives HERE, never the viewport.
.doc-md keeps its centered reading column inside (72rem — wider
than the 1100px panel's inner width, which stays its effective
ceiling); .doc-raw keeps its own overflow-x. tabindex="-1" in the
markup is the JS focus target. */
.doc-modal-content {
flex: 1;
min-height: 0;
overflow: auto;
padding: 1rem 1.25rem 1.5rem;
}
/* Themed horizontal scrollbar for the modal's raw code block (phase 116,
task 01): the .doc-raw pre already owns its overflow (its own
`overflow-x: auto` + `white-space: pre` — locked A1, code never
wraps), so the native light-gray bar a long line reveals is the pre's
own. Both engine families are themed, scoped to THIS modal only (no
global scrollbar restyle — the TODO's scope): scrollbar-color / -width
for Firefox, the ::-webkit-scrollbar pseudos for Chromium (height 8px
= the horizontal bar; the pre does not scroll vertically). The tokens
follow the saved theme via the :root color-mix derivation; the muted
thumb keeps >= 3:1 non-text contrast on the near-surface track. */
.doc-modal .doc-raw {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.doc-modal .doc-raw::-webkit-scrollbar { height: 8px; }
.doc-modal .doc-raw::-webkit-scrollbar-track { background: var(--scrollbar-track); }
.doc-modal .doc-raw::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 4px;
}
.doc-modal-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); }
/* No motion under reduced motion (same pattern as the phase-25 layers). */
@media (prefers-reduced-motion: reduce) {
.doc-modal-backdrop { transition: none; }
}
/* ---------- Doc edit screen (phase 59, task 06) ----------
/doc-edit.html: the admin-gated edit screen for a doc draft (title,
in-repo path, markdown body) — a FLOW page, not one of the app's
pages, so the header is SLIM (brand + "← Back to chat" only). The
shell rides the full 72rem container like every other page (owner
instruction 2026-09-12 — the phase-59 form-column cap is
superseded). The .sources-gate gate is reused verbatim
(phases 16/35/50). Every pair reuses the Phase-08 AA palette;
touch targets >=44px; :focus-visible via the global 3px outline
rule. No CDN, system fonts. */
.doc-edit-shell {
width: 100%;
display: flex;
flex-direction: column;
gap: 1.25rem;
flex: 1;
}
/* The slim header's back link — the .doc-back ghost language
(document.html): >=44px target, --line border, ink-soft (5.1:1 on
the --surface bar) rising to ink on hover; pushed to the bar's right
edge by the header-inner flex (margin-left: auto — the nav's
own pattern). */
.doc-edit-back {
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-height: 44px;
margin-left: auto;
padding: 0.4rem 0.9rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font-weight: 600;
text-decoration: none;
}
.doc-edit-back:hover { color: var(--ink); border-color: var(--ink-soft); }
.doc-edit-back svg { width: 16px; height: 16px; display: block; }
/* The edit form — the tuning form's card as a vertical stack: labeled
title input, mono path input, the mono markdown textarea (min-height
20rem — the body is the star), and the actions row (the brand Push
button + the back link). Inset fields (bg fill on the surface card). */
#doc-edit-form {
display: flex;
flex-direction: column;
gap: 0.9rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.5rem 1.5rem 1.75rem;
}
#doc-edit-form label {
font-weight: 600;
font-size: 0.95rem;
}
#draft-title,
#draft-path {
width: 100%;
font: inherit;
font-size: 1rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.55rem 0.75rem;
min-height: 44px;
}
/* The in-repo path is machine data — mono (the git-sources URL-input
convention). */
#draft-path {
font-family: var(--mono);
font-size: 0.92rem;
}
/* The markdown body: mono, tall (min-height 20rem), vertical resize.
ink on bg = 16.7:1. */
#draft-body {
width: 100%;
font-family: var(--mono);
font-size: 0.92rem;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.75rem 0.9rem;
min-height: 20rem;
resize: vertical;
}
/* Actions row: the primary Push button (brand, dark ink on brand
5.2:1 — never white on brand) + the Discard control (phase 115 —
the secondary destructive action) + the back link; wraps at narrow
widths. */
.doc-edit-actions {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
#push-doc-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
border: 0;
border-radius: var(--radius-sm);
background: var(--brand);
color: var(--bg); /* dark ink on brand: 5.2:1 */
font: inherit;
font-weight: 700;
cursor: pointer;
padding-inline: 1.25rem;
}
#push-doc-btn:hover:not(:disabled) { background: var(--brand-hover); }
#push-doc-btn:disabled { opacity: 0.6; cursor: wait; }
/* Phase 115 (task 02): the Discard control — the SECONDARY destructive
action, visually subordinate to the brand primary (the ghost family
of .steering-delete / .tuning-delete): ink-soft on transparent
(5.1:1 on --surface — more on the darker --bg canvas) with the
--line border; hover joins the err family (err-ink on err-bg 9.3:1,
the err-line border — the state stays text + color, never color
alone); the 44px touch floor, the global 3px :focus-visible ring,
and the :disabled state is the "Discarding…" in-flight affordance. */
.discard-draft {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--ink-soft);
font: inherit;
font-weight: 600;
font-size: 0.85rem;
white-space: nowrap;
padding-inline: 1rem;
cursor: pointer;
}
.discard-draft:hover:not(:disabled) {
background: var(--err-bg);
color: var(--err-ink);
border-color: var(--err-line);
}
.discard-draft:disabled { opacity: 0.6; cursor: wait; }
/* The success status line (role=status): the ok family (ok-ink on
ok-bg 10.6:1) when a push outcome has landed — min-height holds the
line's space so the layout never jumps when the text lands. Empty
(before the first push, or after a failure cleared the stale line)
it is the dashed placeholder (the #archive-upload-result language). */
.doc-edit-status {
margin: 0;
min-height: 1.5rem;
background: var(--ok-bg);
color: var(--ok-ink);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.45rem 0.8rem;
font-size: 0.9rem;
font-weight: 600;
}
.doc-edit-status:empty {
background: transparent;
border-style: dashed;
color: var(--ink-soft);
}
/* The error banner (role=alert): the err family (err-ink on err-bg
9.3:1, err-line border) — git's stderr may carry long paths, so
long words break instead of overflowing the card. */
.doc-edit-error {
margin: 0;
background: var(--err-bg);
color: var(--err-ink);
border: 1px solid var(--err-line);
border-radius: var(--radius-sm);
padding: 0.5rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
overflow-wrap: anywhere;
}
/* ---------- Footer ---------- */
.app-footer {
border-top: 1px solid var(--line);
background: var(--surface);
padding-block: 0.8rem;
margin-top: 1rem;
}
.footer-inner {
display: flex;
justify-content: space-between;
gap: 1rem;
color: var(--ink-soft);
font-size: 0.85rem;
}
/* ---------- Responsive (tablet squeeze — phase 34 task 04 visual pass) ----------
The full admin bar (brand + nav [Chat, Sources, Tuning] + Tuning
toggle + New chat + Sign out) outgrows a 768px bar in
the desktop styles. Squeeze the pills/gaps moderately — the 44px
touch floor is held by min-height and the 64px height by --header-h
— and let the brand wordmark (base ellipsis) absorb any remainder.
The ≤640 block below stays tighter and wins at phone widths. */
@media (max-width: 900px) {
.header-inner { gap: 0.6rem; }
/* Phase 35: the admin-only "Git sources" link joins the nav — a
fourth text pill on the admin bar — so the tablet squeeze tightens
once more to hold 768px in the admin state (brand already the
designated clip target, pills squeeze next). */
.nav-link { padding: 0.4rem 0.5rem; font-size: 0.85rem; }
.app-nav { gap: 0.15rem; }
.new-chat-btn, .auth-link { padding: 0.45rem 0.5rem; }
}
/* ---------- Responsive (mobile-first adjustments) ---------- */
@media (max-width: 640px) {
:root { --header-h: 58px; }
.container { padding-inline: 0.9rem; }
/* Owner report 2026-09-08: the hamburger "won't extend" on a real
phone while the same width in a desktop browser works. The page
opts into `viewport-fit=cover` (the ), so on notched phones
the layout viewport runs UNDER the status bar / Dynamic Island —
and this bar (58px, toggle tap zone y≈7–51px) sits entirely in the
system top-edge zone: visually under the battery/signal icons, and
in the region where a real device does not reliably deliver taps
to the page (desktop emulation has no system top zone, so it works
there — the device-only failure). Pad the bar DOWN by the top
safe-area inset: the header surface still fills the notch region
(that is the point of viewport-fit=cover) but every control — the
#nav-toggle first — lands in the reliably tappable area, and the
dropdown's top:100% follows the taller bar automatically. The
.app-header rule doubles as the document viewer's row-1 bar (the
same class), so the viewer's hamburger is covered by the same
rule. env() resolves to 0 on every non-notch surface (desktop,
headless, no-island phones) — byte-identical rendering there. */
.app-header {
height: calc(var(--header-h) + env(safe-area-inset-top, 0px));
padding-top: env(safe-area-inset-top, 0px);
}
/* The share toast rests just under the bar — the bar is taller by
the same inset on notched phones, so the offset must carry it too
(the toast would otherwise tuck under the padded bar). */
.toast {
top: calc(var(--header-h) + 0.75rem + env(safe-area-inset-top, 0px));
}
/* Phase 14: the New chat pill joins the header — tighten the bar so
brand + nav + pill fit at 360px without horizontal overflow (the
brand text may ellipsize as the designated squeeze target). */
/* Phase 29: the admin-only Tuning link joins the nav — three text
pills on the chat bar in the admin state. The brand text (the
designated squeeze target) is already at zero, so the bar squeezes
the nav pills + gaps instead: tighter pill padding/font and a
tighter inner gap. The 44px touch floor is held by min-height, the
58px bar height is pinned by --header-h, and the bar now fits both
auth states at 375px (and 360px) without horizontal overflow.
Phase 34 task 04 (visual pass): the full admin bar — nav [Chat,
Sources, Tuning] + Tuning toggle + Sync + New chat + Sign out —
now ships on EVERY page, so the squeeze tightens once more
(0.3rem inner gap, 0.78rem nav pills, 0.35rem pill padding,
18px brand mark) to hold 375px with the brand mark intact and
360px with the brand clipped clean (overflow:hidden — the mark
never overlaps the nav). */
/* Phase 35: the admin-only "Git sources" link joins the nav — a
fourth text pill on the admin bar — so the mobile squeeze tightens
once more (0.25rem inner gap, 0.72rem nav pills, 0.3rem pill
padding, 0.05rem nav gap) to hold 375px — and 360px, the brand
fully clipped — in the admin state without horizontal overflow. */
.header-inner { gap: 0.25rem; }
.brand { min-width: 0; overflow: hidden; }
.brand-mark { width: 18px; height: 18px; }
.brand-text {
font-size: 0.8rem;
letter-spacing: 0.04em;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Phase 46 (owner permission 2026-08-27, TODO.md L9): the nav links
LEAVE the bar at phone widths — the old pill-squeeze rules for
.nav-link / .app-nav (0.72rem pills, 0.05rem gap, in place of this
comment) are superseded by the #nav-toggle dropdown below. The
action pills' squeeze rules further down are untouched, and the
900px tablet block keeps squeezing the INLINE nav at 641–900px
(the hamburger is absent there). */
.nav-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
padding: 0;
color: var(--ink);
background: none;
border: 0;
border-radius: var(--radius-sm);
cursor: pointer;
touch-action: manipulation; /* Phase 88: standard dead-mobile-button fix — the
toggle's taps must not wait on the double-tap-zoom/pinch disambiguation
(owner bug report 2026-09-08: dead hamburger on chat-page cold boots). */
}
.nav-toggle:hover { background: var(--brand-soft); color: var(--brand-ink); }
/* The icon is sized (an unsized inline SVG would default to 300px
and blow the bar out); 20px reads as a proper hamburger inside
the 44px target. */
.nav-toggle svg { width: 20px; height: 20px; display: block; }
/* The nav becomes the dropdown. The containing block is the sticky
.app-header (.header-inner is not positioned), so the menu spans
the header's full width — edge to edge — intended on mobile;
z-index 21 = header (20) + 1, above the bar content. */
.app-nav {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-left: 0;
flex-direction: column;
gap: 0;
background: var(--surface);
border-bottom: 1px solid var(--line);
box-shadow: var(--shadow-lg);
padding: 0.5rem 0;
z-index: 21;
/* Closed state (default): invisible and non-interactive — task 02's
header.js is the only opener (.is-open + aria-expanded). */
visibility: hidden;
opacity: 0;
transform: translateY(-8px);
pointer-events: none;
transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 180ms;
}
.app-nav.is-open {
visibility: visible;
opacity: 1;
transform: none;
pointer-events: auto;
transition: opacity 180ms ease, transform 180ms ease, visibility 0s;
}
/* Phase 88: while the mobile menu is open, the chat's sticky bottom
cluster is hidden — it is the other positioned/layered element on
the page and must not compete for taps with the open menu (it also
overlaps the menu's lower rows on short viewports — menu y58→417
vs cluster top y395 at 390×600). visibility (not display): layout
is preserved, so closing never reflows the chat column. */
body.nav-menu-open .chat-bottom {
visibility: hidden;
}
/* Menu rows: comfortable ≥44px targets (0.75rem × 2 + the 1rem line)
and readable text — replaces the old .nav-link pill squeeze. */
.app-nav .nav-link { padding: 0.75rem 1.25rem; font-size: 1rem; }
.new-chat-btn { padding: 0.4rem 0.3rem; }
.new-chat-label { display: none; }
.new-chat-btn svg { display: block; }
/* Phase 51: the Share pill squeezes with New chat (same family, same
rules — the chat-shell override below keeps the label visible). */
.share-chat-btn { padding: 0.4rem 0.3rem; }
.share-chat-label { display: none; }
.share-chat-btn svg { display: block; }
/* But on the chat page there is room — keep the label visible and
hide the icon (the button lives inside .chat-shell, not the navbar). */
.chat-shell .new-chat-label { display: inline; }
.chat-shell .new-chat-btn svg { display: none; }
.chat-shell .share-chat-label { display: inline; }
.chat-shell .share-chat-btn svg { display: none; }
/* Phase 55 (task 05, TODO.md L6, A5): the action row flips to a
vertical stack at the phone breakpoint — full-width pills, New
chat above Share. The pill rules above (padding, the
icon/label handling, the .chat-shell label overrides) apply to
the stacked pills unchanged. */
.chat-actions { flex-direction: column; align-items: stretch; gap: 0.5rem; }
/* Phase 53: the stale banner's row wraps at phone width (message
above, action below) — the pill takes a full-width comfortable
row instead of squeezing into the text. */
.stale-regenerate { margin-left: 0; width: 100%; }
.banner-retry { margin-left: 0; width: 100%; }
/* Phase 16: the auth pill goes icon-only like New chat — brand text
ellipsizes as the designated squeeze target, no bar overflow. */
.auth-link { padding: 0.4rem 0.3rem; }
.auth-label { display: none; }
.auth-link svg { display: block; }
/* Phase 46 UX revision: sign-out & sign-in move into the hamburger
dropdown on mobile — hide the bar copies, show the dropdown copies. */
.sign-out-mobile { display: inline-flex; }
#sign-out-btn { display: none !important; }
.sign-in-mobile { display: inline-flex; }
#sign-in-link { display: none !important; }
.nav-toggle { margin-left: auto; }
/* Dropdown-style sign-out: full-width row, error palette, label visible. */
#app-nav .sign-out-btn {
display: flex;
width: 100%;
align-items: center;
gap: 0.6rem;
text-align: left;
padding: 0.75rem 1.25rem;
font-size: 1rem;
border: 0;
border-radius: 0;
background: transparent;
color: var(--err-ink);
min-height: 48px;
}
#app-nav .sign-out-btn:hover {
background: var(--err-bg);
color: var(--err-ink);
}
#app-nav .sign-out-btn svg {
width: 18px;
height: 18px;
flex: 0 0 auto;
}
#app-nav .sign-out-btn .auth-label { display: inline; }
/* Dropdown-style sign-in: matches sign-out row styling. */
#app-nav .sign-in-mobile {
display: flex;
width: 100%;
align-items: center;
gap: 0.6rem;
text-align: left;
padding: 0.75rem 1.25rem;
font-size: 1rem;
border: 0;
border-radius: 0;
background: transparent;
color: var(--err-ink);
font-weight: 600;
text-decoration: none;
min-height: 48px;
}
#app-nav .sign-in-mobile:hover {
background: var(--err-bg);
color: var(--err-ink);
}
#app-nav .sign-in-mobile svg {
width: 18px;
height: 18px;
flex: 0 0 auto;
}
#app-nav .sign-in-mobile .auth-label { display: inline; }
.steering-note { padding: 0.3rem 0.3rem 0.3rem 0.7rem; }
.app-main > .steering-panel { width: calc(100% - 1.8rem); }
.tune-btn { min-height: 44px; }
.retry-btn { min-height: 44px; }
/* Phase 27: the tuning page squeezes like the other cards — the row
padding tightens; the icon+label actions keep their 44px floor and
the note text ellipsizes (min-width: 0). */
.tuning-note { gap: 0.4rem; padding: 0.3rem; }
.tuning-edit, .tuning-delete { padding: 0.35rem 0.5rem; }
.msg-body { max-width: 92%; }
.empty-state { padding: 1.75rem 1.1rem; margin-top: 0.25rem; }
.empty-state-title { font-size: 1.25rem; }
.suggestions { flex-wrap: nowrap; overflow-x: auto; justify-content: flex-start;
padding-bottom: 0.4rem; -webkit-overflow-scrolling: touch; scrollbar-width: thin; }
.suggestion-chip { flex: 0 0 auto; }
.doc-titlebar .container { gap: 0.5rem; } /* phase 34: row 2 squeezes like row 1 at mobile widths */
#doc-title { font-size: 1.1rem; }
.doc-path { max-width: 16rem; }
.doc-md { padding: 1.1rem 1rem; }
.doc-summary { padding: 0.75rem 0.9rem; }
.doc-raw { padding: 1rem; font-size: 0.8rem; }
/* Phase 26: the modal bar squeezes like the other bars — the Full page
pill goes icon-only (aria-label keeps the name), the title clips;
the panel stays 96vw × 92vh, so no horizontal overflow at 360px. */
.doc-modal-header { gap: 0.5rem; padding-inline: 0.9rem; }
.doc-modal-title { font-size: 1.1rem; }
.doc-modal-open { padding: 0.4rem 0.55rem; }
.doc-modal-open svg { display: block; }
.doc-modal-open span { display: none; }
.doc-modal-meta { padding-inline: 0.9rem; }
.doc-modal-content { padding: 0.75rem 0.9rem 1.25rem; }
.composer { padding: 0.5rem; }
/* Phase 35 (phase 49: + the archive upload form): the add forms
stack like the other cards — label, full-width input, full-width
button; the table wrapper's horizontal scroll already covers long
URLs/paths. */
#git-source-form,
#archive-upload-form { flex-direction: column; align-items: stretch; }
#git-source-form > label,
#archive-upload-form > label { white-space: normal; }
#git-source-url,
#git-source-token,
#archive-upload-file { min-width: 0; }
#git-source-add,
#archive-upload-btn { width: 100%; }
/* Phase 50: the History table keeps its full width (the .table-wrap
horizontal scroll already covers it); the actions cell wraps so
the two-step confirm pair fits the phone width. */
.history-actions-cell { white-space: normal; }
.history-actions { flex-wrap: wrap; }
/* Phase 79 task 06: the Tokens create row stacks — the label input
takes the full width, the Generate pill drops under it (full
width); the once field's row stacks the same way; the actions
cell wraps so the two-step confirm pair fits the phone width
(the table's own horizontal scroll covers the columns). */
.token-create { flex-direction: column; align-items: stretch; }
.token-generate { width: 100%; }
.token-once-row { flex-direction: column; align-items: stretch; }
#token-once-copy { width: 100%; }
.tokens-actions-cell { white-space: normal; }
.tokens-actions { flex-wrap: wrap; }
/* Phase 91 task 05: the Theme form squeezes — the color grid drops
to 1 column (the label + swatch pairs keep their ≥44px targets)
and the actions row stacks (Save full width, Reset under it). */
.theme-colors { grid-template-columns: 1fr; }
.theme-actions { flex-direction: column; align-items: stretch; }
#theme-save { width: 100%; }
.theme-reset { width: 100%; }
/* Phase 51: the shared page squeezes like the chat column — the
title and the note step down (the empty-state-title family); the
shell is full-width here (the 72rem cap never binds on a phone —
the container is already 100%) and .msg-body's 92% override
above applies. */
#shared-title { font-size: 1.35rem; }
.shared-note { font-size: 0.88rem; }
.footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; }
main { padding-bottom: env(safe-area-inset-bottom, 0); }
/* Sync + History refresh: full-width LABELED pills on mobile (the
icon-only squeeze is gone — a symbol-only control is too small a
target to discover on a phone). The RAG page-head row wraps so
the Sync pill drops below the "Knowledge base" title at full
width; the History page-head already wraps the pill below its
title block. The Sync label's min(16rem, 40vw) cap lifts so the
live-file text ellipsizes against the FULL width (min-width: 0
lets the nowrap flex item shrink, which is what engages the
ellipsis); the refresh glyph joins its visible label. The
aria-labels keep the accessible names. */
.page-head-row { flex-wrap: wrap; }
.sync-btn { width: 100%; }
.sync-label { max-width: none; min-width: 0; }
.history-refresh { width: 100%; }
.history-refresh svg { display: block; }
.sync-result {
position: absolute !important;
width: 1px; height: 1px;
margin: -1px; padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
}
/* Phase 46: prefers-reduced-motion stills the mobile menu — no
180ms slide+fade; open/close snaps (the visibility/opacity flip
applies instantly) and stays correct. BOTH states are named: the
.is-open rule (0,2,0) would otherwise out-specify a bare .app-nav
(0,1,0) and the OPEN transition would still animate. */
@media (prefers-reduced-motion: reduce) {
.app-nav,
.app-nav.is-open { transition: none; }
}