6fb260892f
An approval card was Allow, Always, or Don't. A model proposing the right command with one flag wrong therefore cost a whole round trip to explain in prose. There is an Edit button on it now. Where the edit lands is the whole of the feature, and it is one line. `arguments` is the list `_run_calls` hands to `run_tool` as `parsed=`, and `run_tool` never re-parses -- so writing into it inside `_authorise` is the only mutation the runner can see. Editing the Item would do nothing: it is frozen and display-only. Two things had to move with it. The raw `call["arguments"]` string is rewritten beside the parsed dict, and the assistant turn is now built *after* `_authorise` rather than before it -- the old order told the model it ran what it proposed while something else ran, and every later round would have reasoned from a transcript that was quietly false. And `_remember_always` reads the edit, or "always allow this" would store a standing permission for a command nobody approved; it still derives the pattern itself through `policy.subject`, which yields nothing for a composed command line. Nothing is re-checked against the mode or the lists, and that is not a shortcut. The deny list resolves to ASK rather than to a refusal -- it means "always ask about this" -- so a person who has typed the command and pressed Allow is exactly the asking it was demanding, and re-asking would put the same card up with no way past it. It is the line the terminal panel already draws. The box is only offered where the detail *is* an argument and can be put back: a tool with no entry in `tool_labels.DETAIL_KEYS` gets a `k=repr(v)` summary, and a box there would silently change nothing. Both halves are always in the DOM with one hidden, rather than the field being created on click -- a field that does not exist until a handler runs is a field that submits nothing if the handler fails, and this one decides what runs on somebody's machine. The transcript says "edited by you". Attributing somebody's own typing to a model is the same misattribution as the other way round. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1333 lines
41 KiB
CSS
1333 lines
41 KiB
CSS
/*
|
|
Chat thread, composer, message bodies and code blocks.
|
|
|
|
Loaded only on chat pages. Like app.css, every value resolves through
|
|
tokens.css.
|
|
*/
|
|
|
|
/* --- Thread --------------------------------------------------------------- */
|
|
.thread-scroll {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
scroll-behavior: smooth;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--border-strong) transparent;
|
|
}
|
|
|
|
.thread {
|
|
max-width: var(--thread-max-width);
|
|
margin: 0 auto;
|
|
padding: var(--sp-6) var(--sp-5) var(--sp-8);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-6);
|
|
}
|
|
|
|
.thread__intro {
|
|
display: grid;
|
|
place-items: center;
|
|
gap: var(--sp-3);
|
|
text-align: center;
|
|
padding: var(--sp-12) 0 var(--sp-6);
|
|
}
|
|
|
|
/* --- Messages ------------------------------------------------------------- */
|
|
.msg {
|
|
display: grid;
|
|
grid-template-columns: 2rem 1fr;
|
|
gap: var(--sp-3);
|
|
align-items: start;
|
|
}
|
|
|
|
.msg__gutter {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border-radius: var(--radius-full);
|
|
overflow: hidden;
|
|
}
|
|
.msg__mark { width: 2rem; height: 2rem; }
|
|
.msg__avatar { width: 2rem; height: 2rem; border-radius: var(--radius); }
|
|
.msg__initial {
|
|
width: 2rem; height: 2rem;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: var(--radius-full);
|
|
background: var(--surface-active);
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.msg__main { min-width: 0; }
|
|
|
|
.msg__meta {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: var(--sp-2);
|
|
margin-bottom: var(--sp-1);
|
|
}
|
|
.msg__author {
|
|
font-family: var(--font-display);
|
|
font-size: var(--text-base);
|
|
font-weight: 600;
|
|
}
|
|
.msg__model {
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
font-family: var(--font-mono);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: 16rem;
|
|
}
|
|
|
|
.msg__body {
|
|
line-height: var(--leading-relaxed);
|
|
overflow-wrap: break-word;
|
|
}
|
|
/* User turns and mid-stream assistant text are plain text, so newlines and
|
|
runs of spaces have to survive. */
|
|
.msg__body--plain { white-space: pre-wrap; }
|
|
|
|
.msg--user .msg__body--plain {
|
|
background: var(--bubble-user);
|
|
padding: var(--sp-3) var(--sp-4);
|
|
border-radius: var(--radius-lg);
|
|
display: inline-block;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.msg__error { margin: var(--sp-2) 0; align-items: flex-start; }
|
|
|
|
/* --- Streaming indicator -------------------------------------------------- */
|
|
/* Shown until the first token arrives, then hidden by the sibling selector
|
|
below -- no JavaScript involved in either direction. */
|
|
.msg__waiting { padding: var(--sp-2) 0; }
|
|
|
|
.dots { display: inline-flex; gap: 0.25rem; align-items: center; }
|
|
.dots i {
|
|
width: 0.4rem;
|
|
height: 0.4rem;
|
|
border-radius: var(--radius-full);
|
|
background: var(--ink-faint);
|
|
animation: dot-pulse 1.3s ease-in-out infinite;
|
|
}
|
|
.dots i:nth-child(2) { animation-delay: 0.18s; }
|
|
.dots i:nth-child(3) { animation-delay: 0.36s; }
|
|
|
|
@keyframes dot-pulse {
|
|
0%, 60%, 100% { opacity: 0.28; transform: translateY(0); }
|
|
30% { opacity: 1; transform: translateY(-2px); }
|
|
}
|
|
|
|
/* A caret trailing the text while it streams. Attached to the last block so it
|
|
sits at the end of the prose rather than on a line of its own. */
|
|
.msg__body--live:not(:empty) > :last-child::after {
|
|
content: "";
|
|
display: inline-block;
|
|
width: 0.45rem;
|
|
height: 1.05em;
|
|
margin-left: 1px;
|
|
vertical-align: text-bottom;
|
|
background: var(--leaf);
|
|
opacity: 0.75;
|
|
animation: caret 1.05s steps(1) infinite;
|
|
}
|
|
@keyframes caret { 0%, 49% { opacity: 0.75; } 50%, 100% { opacity: 0; } }
|
|
|
|
/* --- Reasoning ------------------------------------------------------------ */
|
|
.reasoning {
|
|
margin: 0 0 var(--sp-3);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: color-mix(in srgb, var(--surface) 70%, transparent);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
/*
|
|
A live block is emitted before the first token arrives, and plenty of models
|
|
emit no reasoning at all. Hiding it until it has content means those models
|
|
never show an empty "Thinking" box, and no JavaScript is involved either way.
|
|
*/
|
|
.reasoning--live:not(:has(.reasoning__body:not(:empty))) { display: none; }
|
|
|
|
.reasoning__summary {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
padding: var(--sp-2) var(--sp-3);
|
|
cursor: pointer;
|
|
color: var(--ink-muted);
|
|
list-style: none;
|
|
user-select: none;
|
|
border-radius: var(--radius);
|
|
}
|
|
.reasoning__summary::-webkit-details-marker { display: none; }
|
|
.reasoning__summary:hover { color: var(--ink); background: var(--surface-hover); }
|
|
|
|
/* --- Compacted turns -------------------------------------------------------
|
|
Summarised messages, kept and readable but out of the way. Collapsed by
|
|
default: the point of compacting was that they stopped mattering.
|
|
*/
|
|
.compacted {
|
|
border: 1px dashed var(--border-strong);
|
|
border-radius: var(--radius);
|
|
background: color-mix(in srgb, var(--surface) 50%, transparent);
|
|
}
|
|
.compacted__summary {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
padding: var(--sp-2) var(--sp-3);
|
|
cursor: pointer;
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
list-style: none;
|
|
user-select: none;
|
|
}
|
|
.compacted__summary::-webkit-details-marker { display: none; }
|
|
.compacted__summary:hover { color: var(--ink); }
|
|
.compacted[open] .reasoning__chevron { transform: rotate(180deg); }
|
|
|
|
.compacted__note {
|
|
margin: 0;
|
|
padding: 0 var(--sp-3);
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
line-height: var(--leading-normal);
|
|
}
|
|
.compacted__body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-6);
|
|
padding: var(--sp-4) var(--sp-3);
|
|
opacity: 0.75;
|
|
}
|
|
|
|
/* --- Suggestions -----------------------------------------------------------
|
|
Starting points on the empty screen. Cards rather than a list, because they
|
|
are things to press.
|
|
*/
|
|
.suggestions {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
|
gap: var(--sp-3);
|
|
width: 100%;
|
|
max-width: 40rem;
|
|
margin-top: var(--sp-6);
|
|
text-align: left;
|
|
}
|
|
|
|
.suggestion {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-1);
|
|
padding: var(--sp-3) var(--sp-4);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
background: var(--surface);
|
|
color: var(--ink);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
|
}
|
|
.suggestion:hover { background: var(--surface-hover); border-color: var(--border-strong); }
|
|
.suggestion:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
|
|
.suggestion__name { font-weight: 600; font-size: var(--text-sm); }
|
|
.suggestion__note {
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-muted);
|
|
line-height: var(--leading-normal);
|
|
}
|
|
|
|
/* Something is happening to the whole thread rather than to one message --
|
|
compaction, which makes a model call and can take a while. Shaped like a
|
|
turn so it sits in the column rather than floating over it. */
|
|
.thread__working {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-3) 0;
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
/* --- Metrics ---------------------------------------------------------------
|
|
What a reply cost, under the bubble. Quiet by default: it is reference, not
|
|
something to read every time.
|
|
*/
|
|
.msg__metrics {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: var(--sp-3);
|
|
margin-top: var(--sp-2);
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.msg__metrics:empty { display: none; }
|
|
|
|
.metric { display: inline-flex; align-items: center; gap: var(--sp-1); cursor: default; }
|
|
|
|
.metric__bar {
|
|
display: inline-block;
|
|
width: 3rem;
|
|
height: 0.3rem;
|
|
border-radius: var(--radius-full);
|
|
background: var(--surface-active);
|
|
overflow: hidden;
|
|
}
|
|
.metric__fill {
|
|
display: block;
|
|
height: 100%;
|
|
background: var(--ink-faint);
|
|
transition: width var(--transition);
|
|
}
|
|
.metric--context.is-warning { color: var(--warning); }
|
|
.metric--context.is-warning .metric__fill { background: var(--warning); }
|
|
.metric--context.is-danger { color: var(--danger); }
|
|
.metric--context.is-danger .metric__fill { background: var(--danger); }
|
|
|
|
.reasoning__icon { color: var(--leaf); flex: none; }
|
|
.reasoning__label { flex: 1; font-style: italic; }
|
|
|
|
.reasoning__chevron {
|
|
flex: none;
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
.reasoning[open] .reasoning__chevron { transform: rotate(180deg); }
|
|
|
|
.reasoning__body {
|
|
padding: 0 var(--sp-3) var(--sp-3);
|
|
margin-left: var(--sp-2);
|
|
border-left: 2px solid var(--border-strong);
|
|
padding-left: var(--sp-3);
|
|
white-space: pre-wrap;
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
line-height: var(--leading-relaxed);
|
|
max-height: 26rem;
|
|
overflow-y: auto;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
/* Gentle pulse on the icon while thinking is still streaming. */
|
|
.reasoning--live .reasoning__icon { animation: think-pulse 1.6s ease-in-out infinite; }
|
|
@keyframes think-pulse {
|
|
0%, 100% { opacity: 0.45; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
/* --- Tool activity --------------------------------------------------------
|
|
Deliberately the same object as the reasoning block: both answer "what did
|
|
it do before it replied", and giving them two visual languages would suggest
|
|
a difference that is not there. */
|
|
.tool-activity-list:empty { display: none; }
|
|
|
|
.tool-activity {
|
|
margin: 0 0 var(--sp-3);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: color-mix(in srgb, var(--surface) 70%, transparent);
|
|
font-size: var(--text-sm);
|
|
}
|
|
.tool-activity--error { border-color: var(--danger); }
|
|
|
|
.tool-activity__summary {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
padding: var(--sp-2) var(--sp-3);
|
|
cursor: pointer;
|
|
color: var(--ink-muted);
|
|
list-style: none;
|
|
user-select: none;
|
|
border-radius: var(--radius);
|
|
}
|
|
.tool-activity__summary::-webkit-details-marker { display: none; }
|
|
.tool-activity__summary:hover { color: var(--ink); background: var(--surface-hover); }
|
|
.tool-activity__icon { color: var(--leaf); flex: none; }
|
|
.tool-activity__label { flex: 1; min-width: 0; }
|
|
.tool-activity__count { color: var(--ink-faint); }
|
|
/* The model's own account of what a call is for, under the command it belongs
|
|
to. `display: block` inside the flex row's label, so the icon and the chevron
|
|
stay centred against both lines. */
|
|
.tool-activity__why {
|
|
display: block;
|
|
color: var(--ink-faint);
|
|
font-size: var(--text-xs);
|
|
}
|
|
.tool-activity[open] .reasoning__chevron { transform: rotate(180deg); }
|
|
|
|
.tool-activity__body {
|
|
padding: 0 var(--sp-3) var(--sp-3);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-3);
|
|
}
|
|
.tool-activity__error { margin: 0; color: var(--ink-muted); }
|
|
|
|
.tool-result {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
padding-left: var(--sp-3);
|
|
border-left: 2px solid var(--border-strong);
|
|
min-width: 0;
|
|
}
|
|
.tool-result__title {
|
|
color: var(--accent);
|
|
font-weight: 500;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.tool-result__host { color: var(--ink-faint); font-size: var(--text-xs); }
|
|
.tool-result__snippet {
|
|
margin: 0;
|
|
color: var(--ink-muted);
|
|
line-height: var(--leading-relaxed);
|
|
}
|
|
|
|
/* What a tool returned, verbatim. Preformatted rather than rendered: this is
|
|
third-party text and markdown is the one path allowed to emit HTML. */
|
|
.tool-result__text {
|
|
margin: 0;
|
|
padding: var(--sp-3);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-sunken);
|
|
color: var(--ink-muted);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
line-height: var(--leading-relaxed);
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
max-height: 22em;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* A diff, from a write or an update.
|
|
|
|
The same visual language as .tool-result__text above -- both answer "what did
|
|
it do", and two languages would suggest a difference that is not there. The
|
|
colours are --success and --danger rather than anything new: --success is
|
|
deliberately a different hue from --leaf so an added line does not read as the
|
|
brand accent, and --danger is already what an error border uses, so a removed
|
|
line reads as removed rather than as broken.
|
|
|
|
The padding is on the line, not on the block, so a highlighted row runs the
|
|
full width instead of stopping short of the rounded corner. */
|
|
.diff {
|
|
margin: 0;
|
|
padding: var(--sp-2) 0;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-sunken);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
line-height: var(--leading-relaxed);
|
|
max-height: 26em;
|
|
overflow: auto;
|
|
}
|
|
.diff__line {
|
|
display: block;
|
|
padding: 0 var(--sp-3);
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.diff__line--add { background: var(--success-soft); color: var(--success); }
|
|
.diff__line--del { background: var(--danger-soft); color: var(--danger); }
|
|
.diff__line--meta { color: var(--ink-faint); }
|
|
.diff__line--ctx { color: var(--ink-muted); }
|
|
|
|
/* --- The model asking you something --------------------------------------- */
|
|
/* Attributed to the model on purpose. A card styled like the application is a
|
|
card people answer with things they would not tell a chatbot. */
|
|
.interaction {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-3);
|
|
margin: var(--sp-3) 0;
|
|
padding: var(--sp-4);
|
|
border: 1px solid var(--accent);
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface);
|
|
}
|
|
.interaction__from {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
margin: 0;
|
|
color: var(--ink-faint);
|
|
font-size: var(--text-xs);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
.interaction__form { display: flex; flex-direction: column; gap: var(--sp-4); }
|
|
/* One block per question. Several go on one card and submit together. */
|
|
.interaction__question {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-2);
|
|
margin: 0;
|
|
padding: 0;
|
|
border: 0;
|
|
min-width: 0;
|
|
}
|
|
.interaction__question + .interaction__question {
|
|
padding-top: var(--sp-4);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.interaction__title { margin: 0; padding: 0; color: var(--ink); font-weight: 500; }
|
|
.interaction__options { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
|
|
.interaction__note { color: var(--ink-faint); font-size: var(--text-xs); }
|
|
|
|
/* An option. A radio, so picking one unpicks the last, but shaped like the
|
|
button it reads as. */
|
|
.chip { position: relative; display: inline-flex; }
|
|
.chip input {
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
margin: 0;
|
|
}
|
|
.chip span {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: var(--control-h);
|
|
padding: 0 var(--sp-3);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--surface-raised);
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
}
|
|
.chip input:hover + span { background: var(--surface-hover); color: var(--ink); }
|
|
.chip input:checked + span {
|
|
border-color: var(--accent);
|
|
background: var(--surface-active);
|
|
color: var(--ink);
|
|
}
|
|
.chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
.interaction__detail {
|
|
margin: 0;
|
|
padding: var(--sp-3);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-sunken);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
/* The same command, correctable. Sized and spaced like the <pre> it replaces
|
|
so pressing Edit does not make the card jump. */
|
|
.interaction__edit {
|
|
width: 100%;
|
|
margin: 0 0 var(--sp-2);
|
|
padding: var(--sp-3);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
resize: vertical;
|
|
}
|
|
.interaction__reason { margin: 0; color: var(--ink-muted); font-size: var(--text-xs); }
|
|
/* The model's account of what it is about to do. Above the command and quieter
|
|
than the title, so the command stays the thing being agreed to. */
|
|
.interaction__purpose { margin: 0; color: var(--ink-muted); font-size: var(--text-sm); }
|
|
.interaction__actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--sp-2);
|
|
align-items: center;
|
|
}
|
|
.interaction__write { display: flex; gap: var(--sp-2); flex: 1 1 16rem; min-width: 0; }
|
|
.interaction__write .input { flex: 1; min-width: 0; }
|
|
|
|
/* --- Stop, notes and editing ---------------------------------------------- */
|
|
.msg__status { font-size: var(--text-xs); color: var(--ink-faint); font-style: italic; }
|
|
.msg__status:empty { display: none; }
|
|
|
|
.msg__waiting {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-2) 0;
|
|
}
|
|
/* Once the answer has content the caret carries the "still going" signal, so
|
|
the dots go, but Stop must stay reachable until the stream ends. */
|
|
.msg__body--live:not(:empty) + .msg__waiting .dots { display: none; }
|
|
|
|
/* Send and Stop are one button. Which icon shows is decided here rather than
|
|
in JavaScript, so the state is visible in the markup and the swap is free. */
|
|
.composer__icon { display: flex; }
|
|
[data-composer-action="send"] .composer__icon--stop,
|
|
[data-composer-action="stop"] .composer__icon--send { display: none; }
|
|
|
|
.composer__btn[data-composer-action="stop"] {
|
|
background: var(--danger);
|
|
border-color: var(--danger);
|
|
color: var(--ink-inverse);
|
|
}
|
|
.composer__btn[data-composer-action="stop"]:hover:not(:disabled) {
|
|
background: var(--danger-hover);
|
|
border-color: var(--danger-hover);
|
|
}
|
|
/* The microphone is the same shape: one button, state in a data attribute. */
|
|
[data-mic-state="idle"] .composer__icon--recording,
|
|
[data-mic-state="working"] .composer__icon--recording,
|
|
[data-mic-state="recording"] .composer__icon--mic { display: none; }
|
|
|
|
.composer__mic[data-mic-state="recording"] {
|
|
background: var(--danger);
|
|
border-color: var(--danger);
|
|
color: var(--ink-inverse);
|
|
animation: mic-pulse 1.6s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes mic-pulse {
|
|
0%, 100% { box-shadow: 0 0 0 0 var(--accent-soft); }
|
|
50% { box-shadow: 0 0 0 5px transparent; }
|
|
}
|
|
|
|
/* Play and stop on the read-aloud button, chosen by the class audio.js sets. */
|
|
.speak__icon { display: flex; }
|
|
[data-speak] .speak__icon--stop,
|
|
[data-speak].is-speaking .speak__icon--play { display: none; }
|
|
[data-speak].is-speaking .speak__icon--stop { display: flex; }
|
|
[data-speak].is-speaking { color: var(--accent); }
|
|
|
|
.composer__stop-square {
|
|
width: 0.7rem;
|
|
height: 0.7rem;
|
|
border-radius: 2px;
|
|
background: currentColor;
|
|
}
|
|
|
|
.msg__note {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
margin: var(--sp-2) 0 0;
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
font-style: italic;
|
|
}
|
|
|
|
.msg--editing .msg__main { width: 100%; }
|
|
.edit-form { display: flex; flex-direction: column; gap: var(--sp-3); }
|
|
.edit-form .textarea { min-height: 4rem; }
|
|
|
|
/* --- Message actions ------------------------------------------------------ */
|
|
.msg__actions {
|
|
display: flex;
|
|
gap: var(--sp-1);
|
|
margin-top: var(--sp-2);
|
|
opacity: 0;
|
|
transition: opacity var(--transition-fast);
|
|
}
|
|
.msg:hover .msg__actions,
|
|
.msg:focus-within .msg__actions { opacity: 1; }
|
|
.msg__actions .is-copied { color: var(--success); }
|
|
|
|
/* --- A turn that is waiting to be sent ------------------------------------
|
|
Its actions do not fade in on hover like the others: they are the only way
|
|
to withdraw something that has not happened yet, and a control you have to
|
|
find by hovering is one somebody will not find. */
|
|
.msg--queued { opacity: 0.75; }
|
|
.msg--queued .msg__body {
|
|
border-inline-start: 2px dashed var(--border-strong);
|
|
padding-inline-start: var(--sp-2);
|
|
}
|
|
.msg__actions--queued { opacity: 1; align-items: center; }
|
|
.msg__note {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
/* --- Rendered Markdown ---------------------------------------------------- */
|
|
.msg__body > :first-child { margin-top: 0; }
|
|
.msg__body > :last-child { margin-bottom: 0; }
|
|
|
|
.msg__body h1, .msg__body h2, .msg__body h3,
|
|
.msg__body h4, .msg__body h5, .msg__body h6 {
|
|
margin: var(--sp-5) 0 var(--sp-2);
|
|
}
|
|
.msg__body h1 { font-size: var(--text-xl); }
|
|
.msg__body h2 { font-size: var(--text-lg); }
|
|
.msg__body h3 { font-size: var(--text-md); }
|
|
|
|
.msg__body ul, .msg__body ol { margin: 0 0 var(--sp-4); padding-left: var(--sp-6); }
|
|
.msg__body li { margin-bottom: var(--sp-1); }
|
|
|
|
.msg__body blockquote {
|
|
margin: 0 0 var(--sp-4);
|
|
padding: var(--sp-1) var(--sp-4);
|
|
border-left: 3px solid var(--border-strong);
|
|
color: var(--ink-muted);
|
|
font-style: italic;
|
|
}
|
|
|
|
.msg__body hr { border: 0; border-top: 1px solid var(--border); margin: var(--sp-5) 0; }
|
|
|
|
.msg__body :not(pre) > code {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.875em;
|
|
padding: 0.13em 0.36em;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--code-bg);
|
|
border: 1px solid var(--code-border);
|
|
}
|
|
|
|
.msg__body table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 0 0 var(--sp-4);
|
|
font-size: var(--text-sm);
|
|
display: block;
|
|
overflow-x: auto;
|
|
}
|
|
.msg__body th, .msg__body td {
|
|
border: 1px solid var(--border);
|
|
padding: var(--sp-2) var(--sp-3);
|
|
text-align: left;
|
|
}
|
|
.msg__body th { background: var(--surface); font-weight: 600; }
|
|
|
|
.msg__body img { max-width: 100%; height: auto; border-radius: var(--radius); }
|
|
|
|
/* --- Code blocks ---------------------------------------------------------- */
|
|
.code-block {
|
|
margin: 0 0 var(--sp-4);
|
|
border: 1px solid var(--code-border);
|
|
border-radius: var(--radius);
|
|
background: var(--code-bg);
|
|
overflow: hidden;
|
|
}
|
|
.code-block__label {
|
|
padding: var(--sp-1) var(--sp-3);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
border-bottom: 1px solid var(--code-border);
|
|
background: color-mix(in srgb, var(--code-bg) 60%, var(--surface));
|
|
}
|
|
.code-block__pre {
|
|
margin: 0;
|
|
padding: var(--sp-3) var(--sp-4);
|
|
overflow-x: auto;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-sm);
|
|
line-height: 1.55;
|
|
}
|
|
.code-block__pre code { font-family: inherit; background: none; border: 0; padding: 0; }
|
|
|
|
/*
|
|
Pygments token colours, mapped onto theme tokens rather than a fixed scheme,
|
|
so code follows the active theme. classprefix "pg-" is set in markdown.py.
|
|
*/
|
|
.pg-c, .pg-c1, .pg-cm, .pg-cs, .pg-cp { color: var(--ink-faint); font-style: italic; }
|
|
/* Keywords take --warning, not the brand accent: strings are already green, and
|
|
two greens in one code block is not a colour scheme, it is a bug report. */
|
|
.pg-k, .pg-kn, .pg-kd, .pg-kc, .pg-kr, .pg-kt { color: var(--warning); }
|
|
.pg-s, .pg-s1, .pg-s2, .pg-sb, .pg-sd, .pg-se, .pg-sh, .pg-si, .pg-sx { color: var(--success); }
|
|
.pg-m, .pg-mi, .pg-mf, .pg-mh, .pg-mo { color: var(--danger); }
|
|
.pg-nf, .pg-nd { color: var(--accent); }
|
|
.pg-nc, .pg-nn { color: var(--accent-hover); font-weight: 600; }
|
|
.pg-nb, .pg-bp { color: var(--accent); }
|
|
.pg-nv, .pg-vi, .pg-vg, .pg-vc { color: var(--ink); }
|
|
.pg-o, .pg-ow, .pg-p { color: var(--ink-muted); }
|
|
.pg-err { color: var(--danger); }
|
|
.pg-gd { color: var(--danger); }
|
|
.pg-gi { color: var(--success); }
|
|
|
|
/* --- Composer ------------------------------------------------------------- */
|
|
.composer {
|
|
flex: none;
|
|
padding: var(--sp-3) var(--sp-5) var(--sp-4);
|
|
border-top: 1px solid var(--border);
|
|
background: var(--bg);
|
|
}
|
|
/* position: relative anchors the `@` and `/` menu to the box. */
|
|
.composer__inner {
|
|
position: relative;
|
|
max-width: var(--thread-max-width);
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/*
|
|
A column: chips, then the text across the full width, then one toolbar row.
|
|
|
|
The text gets its own row rather than sharing one with the buttons, which is
|
|
what lets the toolbar hold more than two controls without the input shrinking
|
|
to nothing. The chips stay inside the form so their hidden file_ids inputs are
|
|
submitted with the message.
|
|
*/
|
|
.composer__form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-1);
|
|
padding: var(--sp-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-xl);
|
|
background: var(--surface);
|
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
|
}
|
|
.composer__form:focus-within {
|
|
border-color: var(--accent);
|
|
box-shadow: var(--ring);
|
|
}
|
|
|
|
/*
|
|
The text, and the mirror behind it.
|
|
|
|
Every property that affects where a character lands is declared once, on both
|
|
-- font, size, line height, padding, wrapping. A single difference and the
|
|
rectangles slide off the words they are marking. `font: inherit` on the
|
|
textarea is not enough: a textarea's default font is not the page's.
|
|
*/
|
|
.composer__field { position: relative; }
|
|
|
|
.composer__input,
|
|
.composer__mirror {
|
|
width: 100%;
|
|
min-width: 0;
|
|
margin: 0;
|
|
padding: var(--sp-2) var(--sp-2) var(--sp-1);
|
|
border: 0;
|
|
font-family: var(--font-body);
|
|
font-size: var(--text-base);
|
|
line-height: var(--leading-normal);
|
|
letter-spacing: normal;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: break-word;
|
|
word-break: normal;
|
|
max-height: 20rem;
|
|
}
|
|
|
|
.composer__input {
|
|
position: relative;
|
|
display: block;
|
|
background: none;
|
|
resize: none;
|
|
color: var(--ink);
|
|
overflow-y: auto;
|
|
}
|
|
.composer__input:focus { outline: none; }
|
|
|
|
/* Behind, and contributing nothing but background rectangles: its own text is
|
|
transparent, so the glyphs on screen are always the textarea's. That is what
|
|
makes a pixel of drift invisible rather than a doubled letter. */
|
|
.composer__mirror {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 0;
|
|
overflow: hidden;
|
|
color: transparent;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
.composer__mirror .tok-mention,
|
|
.composer__mirror .tok-command {
|
|
border-radius: var(--radius-sm);
|
|
/* Restated rather than inherited. `color: transparent` on the mirror is an
|
|
inherited value, and a colour the span declares itself beats it -- which is
|
|
exactly what the transcript's rule below used to do from across the file,
|
|
painting the token in accent-coloured mono at 0.95em on top of the
|
|
textarea's own text. Doubled, and shifted from there on, because the
|
|
metrics differ. The font must be restated for the same reason. */
|
|
color: transparent;
|
|
font: inherit;
|
|
/* Bled sideways by a shadow, not by padding: a rectangle that spreads cannot
|
|
move a glyph, and the negative margin that used to do this was the only
|
|
thing in the mirror that could. */
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
.composer__mirror .tok-mention {
|
|
background: var(--accent-soft);
|
|
box-shadow: 0 0 0 2px var(--accent-soft);
|
|
}
|
|
.composer__mirror .tok-command {
|
|
background: var(--leaf-soft);
|
|
box-shadow: 0 0 0 2px var(--leaf-soft);
|
|
}
|
|
|
|
/* The same two in a sent message, where they are text rather than a backdrop --
|
|
and scoped to it, because unscoped they also matched the mirror's spans. */
|
|
.msg .tok-mention {
|
|
border-radius: var(--radius-sm);
|
|
padding: 0 2px;
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
font-family: var(--font-mono);
|
|
font-size: 0.95em;
|
|
}
|
|
|
|
/* Everything that acts on the message, on one line under it. ONE line, always.
|
|
|
|
It used to wrap, and .composer__actions is last in the DOM with
|
|
margin-left:auto -- so the moment an agent chat added a connection, a
|
|
directory and a mode to this row, Send and the microphone were what dropped
|
|
to a second line. There are no media queries in this file, deliberately, and
|
|
the fix is not to add one: it is to say which child gives. */
|
|
.composer__toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: nowrap;
|
|
gap: var(--sp-2);
|
|
min-width: 0;
|
|
}
|
|
.composer__tools { display: flex; align-items: center; gap: var(--sp-1); flex: none; }
|
|
|
|
/* Never shrinks, never wraps, always at the end of the line. This is where the
|
|
hand is going. */
|
|
.composer__actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
flex: none;
|
|
margin-left: auto;
|
|
}
|
|
[data-effort] { flex: none; }
|
|
|
|
/* The one thing allowed to give. It shrinks past its content and scrolls
|
|
sideways rather than wrapping. The scrollbar is hidden: the controls are
|
|
already visibly cut off, and a scrollbar under a --control-h row would change
|
|
the row's height, which is the one thing --control-h exists to prevent. */
|
|
.composer__context {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: nowrap;
|
|
gap: var(--sp-2);
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
overflow-x: auto;
|
|
scrollbar-width: none;
|
|
}
|
|
.composer__context::-webkit-scrollbar { display: none; }
|
|
.composer__agent { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: nowrap; }
|
|
|
|
/* Floors, not fixed widths: a select narrower than this shows no text at all,
|
|
which is worse than the scrolling it was avoiding. */
|
|
.composer__context .select { flex: 0 1 auto; min-width: 6rem; }
|
|
/* `.segmented` already declares flex: none further down, where it is defined. */
|
|
|
|
/* Round, and the same size as each other: attach and send read as one pair
|
|
bracketing the row. */
|
|
.composer__btn { flex: none; border-radius: var(--radius-full); }
|
|
|
|
/* The directory, on a new chat. Monospace because it is a path, and it grows
|
|
to fit rather than being pinned to a width that truncates every real one. */
|
|
.composer__dir {
|
|
flex: 0 1 16rem;
|
|
min-width: 5rem;
|
|
font-family: var(--font-mono);
|
|
font-weight: 400;
|
|
}
|
|
.composer__dir-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
.composer__hint {
|
|
margin: var(--sp-2) 0 0;
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
text-align: center;
|
|
}
|
|
.composer__hint-link {
|
|
border: 0;
|
|
padding: 0;
|
|
background: none;
|
|
font: inherit;
|
|
color: var(--ink-muted);
|
|
text-decoration: underline dotted;
|
|
text-underline-offset: 2px;
|
|
cursor: pointer;
|
|
}
|
|
.composer__hint-link:hover { color: var(--accent); }
|
|
|
|
/*
|
|
The menu `@` and `/` open.
|
|
|
|
Above the composer, not below it: the composer is already at the bottom of
|
|
the window, so anything dropping down would be off screen -- the same reason
|
|
the attach menu is `picker--up`. Anchored to .composer__inner so it lines up
|
|
with the box rather than with the caret; a caret-following menu is nicer and
|
|
needs measuring text in a textarea, which cannot be done without a hidden
|
|
mirror element.
|
|
*/
|
|
.composer-menu {
|
|
position: absolute;
|
|
bottom: calc(100% + var(--sp-2));
|
|
left: 0;
|
|
right: 0;
|
|
z-index: var(--z-dropdown);
|
|
max-height: min(20rem, 45vh);
|
|
overflow-y: auto;
|
|
scrollbar-width: thin;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
background: var(--surface-raised);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
/* What the keys do, pinned to the bottom of the menu. */
|
|
.composer-menu__keys {
|
|
position: sticky;
|
|
bottom: 0;
|
|
padding: var(--sp-1) var(--sp-3);
|
|
border-top: 1px solid var(--border);
|
|
background: var(--surface-raised);
|
|
color: var(--ink-faint);
|
|
font-size: var(--text-xs);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.picker__group {
|
|
margin: 0;
|
|
padding: var(--sp-2) var(--sp-3) var(--sp-1);
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
color: var(--ink-faint);
|
|
}
|
|
|
|
/* One thing this chat has been told to stop asking about. Monospace because an
|
|
entry is usually a command line, and it is worth being able to read back
|
|
exactly what was agreed to. */
|
|
.picker__allow-entry {
|
|
margin: 0;
|
|
padding: 0 var(--sp-3) var(--sp-1);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-muted);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* The help and usage sheets. */
|
|
.sheet { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
|
|
.sheet td { padding: var(--sp-1) var(--sp-2); vertical-align: top; }
|
|
.sheet td:first-child { white-space: nowrap; color: var(--ink-muted); width: 1%; }
|
|
.sheet tr + tr td { border-top: 1px solid var(--border); }
|
|
|
|
/* --- Folders -------------------------------------------------------------- */
|
|
.folder__row { padding-right: var(--sp-1); }
|
|
.folder__toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: none;
|
|
border: 0;
|
|
padding: 0;
|
|
color: inherit;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
}
|
|
.folder__chevron {
|
|
display: inline-flex;
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
.folder__chevron.is-open { transform: rotate(90deg); }
|
|
.folder__contents { padding-left: var(--sp-4); }
|
|
|
|
.nav-item__link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
flex: 1;
|
|
min-width: 0;
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
|
|
/* --- Attachment chips (composer) ------------------------------------------
|
|
Named `attach-chip` and not `chip`, which is the radio pill above. The two
|
|
were both called `.chip` and both set padding, border and max-width, so the
|
|
later block won and the Chat/Agent pills silently inherited the padding of
|
|
an attachment card. Two components with one name is a collision, not a
|
|
family. */
|
|
.composer { position: relative; }
|
|
|
|
.composer__attachments {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--sp-2);
|
|
padding: var(--sp-1) var(--sp-1) 0;
|
|
}
|
|
.composer__attachments:empty { display: none; }
|
|
|
|
.attach-chip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
padding: var(--sp-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
max-width: 20rem;
|
|
font-size: var(--text-sm);
|
|
}
|
|
.attach-chip--error { border-color: var(--danger); background: var(--danger-soft); }
|
|
.attach-chip--error .attach-chip__icon { color: var(--danger); }
|
|
|
|
.attach-chip__thumb {
|
|
display: block;
|
|
width: 2.25rem;
|
|
height: 2.25rem;
|
|
border-radius: var(--radius-sm);
|
|
object-fit: cover;
|
|
flex: none;
|
|
}
|
|
.attach-chip__icon { color: var(--ink-muted); flex: none; display: flex; }
|
|
.attach-chip__body { min-width: 0; flex: 1; display: flex; flex-direction: column; }
|
|
.attach-chip__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.attach-chip__meta { font-size: var(--text-xs); color: var(--ink-faint); }
|
|
.attach-chip__warning { font-size: var(--text-xs); color: var(--danger); }
|
|
|
|
/* --- Drag and drop -------------------------------------------------------- */
|
|
.dropzone-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: var(--z-raised);
|
|
display: none;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--sp-2);
|
|
border: 2px dashed var(--accent);
|
|
border-radius: var(--radius-lg);
|
|
background: color-mix(in srgb, var(--bg) 88%, var(--accent));
|
|
color: var(--accent);
|
|
font-weight: 500;
|
|
/* The overlay must not eat the drop event it is advertising. */
|
|
pointer-events: none;
|
|
}
|
|
.composer.is-dropping .dropzone-overlay { display: flex; }
|
|
|
|
/* --- Attachments in the thread -------------------------------------------- */
|
|
.attachments {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: flex-start;
|
|
gap: var(--sp-2);
|
|
margin-bottom: var(--sp-2);
|
|
}
|
|
/* No frame: an attachment is a picture, and a border around it only ever drew
|
|
at the wrong width. The anchor shrink-wraps its image rather than filling the
|
|
column, and the width/height attributes on the <img> are overridden so a
|
|
small image is shown at its own size instead of being stretched. */
|
|
.attachments__image {
|
|
display: inline-flex;
|
|
max-width: 100%;
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
line-height: 0;
|
|
}
|
|
.attachments__image img {
|
|
display: block;
|
|
width: auto;
|
|
height: auto;
|
|
max-width: min(22rem, 100%);
|
|
max-height: 20rem;
|
|
object-fit: contain;
|
|
}
|
|
.attachments__doc {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--sp-2);
|
|
padding: var(--sp-2) var(--sp-3);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
font-size: var(--text-sm);
|
|
max-width: 100%;
|
|
}
|
|
.attachments__doc-body { display: flex; flex-direction: column; min-width: 0; }
|
|
.attachments__doc-body > a { overflow-wrap: anywhere; }
|
|
|
|
/* --- Model avatars -------------------------------------------------------- */
|
|
.model-avatar {
|
|
width: 2rem;
|
|
height: 2rem;
|
|
flex: none;
|
|
border-radius: var(--radius);
|
|
object-fit: cover;
|
|
background: var(--surface-active);
|
|
}
|
|
.model-avatar--initial {
|
|
display: grid;
|
|
place-items: center;
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
font-size: var(--text-sm);
|
|
/* Hue comes from the model id (see the stable_hue filter); saturation and
|
|
lightness are fixed so every generated badge stays legible in both themes. */
|
|
background: hsl(var(--avatar-hue, 40) 42% 34%);
|
|
color: hsl(var(--avatar-hue, 40) 60% 92%);
|
|
}
|
|
:root[data-theme="shire"] .model-avatar--initial {
|
|
background: hsl(var(--avatar-hue, 40) 46% 82%);
|
|
color: hsl(var(--avatar-hue, 40) 60% 22%);
|
|
}
|
|
|
|
/* --- Theme toggle --------------------------------------------------------- */
|
|
/* Only the icon for the theme you would switch TO is shown. Both live in the
|
|
same button, so the wrapper must not add a line box of its own. */
|
|
.theme-icon { display: flex; }
|
|
:root[data-theme="moria"] .theme-icon--dark { display: none; }
|
|
:root[data-theme="shire"] .theme-icon--light { display: none; }
|
|
|
|
/* Alpine sets x-cloak until it has initialised; without this, collapsed
|
|
folders flash open on every page load. */
|
|
[x-cloak] { display: none !important; }
|
|
|
|
/* --- Unread indicator ------------------------------------------------------ */
|
|
.unread-dot {
|
|
width: 0.5rem;
|
|
height: 0.5rem;
|
|
border-radius: var(--radius-full);
|
|
background: var(--success);
|
|
flex: none;
|
|
/* A ring so it stays visible against the active row's lighter background. */
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--success) 25%, transparent);
|
|
}
|
|
.unread-dot[hidden] { display: none; }
|
|
|
|
/* --- Agent chats -----------------------------------------------------------
|
|
`.select--sm` and `.input--sm` are defined once, in app.css, at
|
|
var(--control-h-sm). A second definition here made every small control on a
|
|
chat page 0.15rem taller than the same control anywhere else -- which is
|
|
exactly the drift --control-h exists to prevent.
|
|
*/
|
|
|
|
/* The connection. Wide enough to read a machine's name without truncating it
|
|
to three characters, which is what it did while `.select--sm` did not exist.
|
|
A floor rather than a fixed width, so a long name still grows. */
|
|
.composer__connection { min-width: 10rem; }
|
|
|
|
/*
|
|
Chat or Agent: one control with two halves, not two buttons that happen to be
|
|
adjacent. Radios underneath, because the choice is permanent and mutually
|
|
exclusive and should read as a fork -- and because a radio group is what a
|
|
screen reader already knows how to announce.
|
|
*/
|
|
.segmented {
|
|
display: inline-flex;
|
|
flex: none;
|
|
padding: 2px;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-full);
|
|
background: var(--bg-sunken);
|
|
}
|
|
.segmented__option { position: relative; display: inline-flex; }
|
|
.segmented__option input {
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
margin: 0;
|
|
}
|
|
.segmented__option span {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
height: calc(var(--control-h-sm) - 2px);
|
|
padding: 0 var(--sp-3);
|
|
border-radius: var(--radius-full);
|
|
color: var(--ink-faint);
|
|
font-size: var(--text-xs);
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
transition: background var(--transition-fast), color var(--transition-fast);
|
|
}
|
|
.segmented__option input:hover + span { color: var(--ink-muted); }
|
|
.segmented__option input:checked + span {
|
|
background: var(--surface-raised);
|
|
color: var(--ink);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
.segmented__option input:focus-visible + span {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 1px;
|
|
}
|
|
/* The sidebar's copy fills its column rather than sitting at its content
|
|
width: it is the heading for everything below it, not a control in a row. */
|
|
.segmented--grow { display: flex; margin: 0 0 var(--sp-3); }
|
|
.segmented--grow .segmented__option { flex: 1; }
|
|
.segmented--grow .segmented__option span { flex: 1; justify-content: center; }
|
|
|
|
/* --- A plan, and the way to carry it out ----------------------------------- */
|
|
.plan {
|
|
margin: var(--sp-3) 0;
|
|
padding: var(--sp-4);
|
|
border: 1px solid var(--border-strong);
|
|
border-left: 3px solid var(--accent);
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface);
|
|
}
|
|
.plan__title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
margin: 0 0 var(--sp-3);
|
|
font-size: var(--text-base);
|
|
color: var(--ink);
|
|
}
|
|
.plan__summary {
|
|
margin: 0 0 var(--sp-3);
|
|
color: var(--ink-muted);
|
|
line-height: var(--leading-relaxed);
|
|
}
|
|
.plan__section, .plan__phase { margin-bottom: var(--sp-4); }
|
|
.plan__heading {
|
|
margin: 0 0 var(--sp-2);
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
color: var(--ink-faint);
|
|
}
|
|
.plan__findings, .plan__objectives {
|
|
margin: 0;
|
|
padding-left: var(--sp-5);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-1);
|
|
color: var(--ink-muted);
|
|
line-height: var(--leading-relaxed);
|
|
}
|
|
.plan__steps {
|
|
margin: 0;
|
|
padding-left: var(--sp-5);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-2);
|
|
color: var(--ink-muted);
|
|
line-height: var(--leading-relaxed);
|
|
}
|
|
.plan__phase:last-of-type .plan__steps { margin-bottom: var(--sp-4); }
|
|
|
|
/* A status is a class, never a character in the text: a tick written into the
|
|
string would be indistinguishable from a tick the model wrote itself. */
|
|
.plan__item--done { color: var(--success); }
|
|
.plan__item--doing { color: var(--ink); font-weight: 500; }
|
|
.plan__item--dropped { color: var(--ink-faint); text-decoration: line-through; }
|
|
.plan__phase--done .plan__heading { color: var(--success); }
|
|
|
|
.plan__note-inline {
|
|
display: block;
|
|
color: var(--ink-faint);
|
|
font-size: var(--text-xs);
|
|
}
|
|
.plan__note { color: var(--ink-faint); font-size: var(--text-xs); }
|