A third child of .shell, opening and closing like the sidebar opposite it, showing the system message that would go out, the tools offered, what the last reply cost, and the whole request body as JSON. Rebuilt, not recorded. Recording every request would store a copy of the growing conversation against every message -- quadratic in chat length -- and the thing an administrator debugging a bad answer actually wants is what the current configuration produces. The panel says exactly that at the top, so nobody mistakes it for forensics. Owner-checked and admin-checked, not admin alone. permissions.resolve giving an admin everything is about configuration, which they can grant themselves anyway; reading someone's conversation is a different act, and it is why sharing.visible_to has no admin branch. An inspector that could dump any user's transcript would be that branch under another name. No new JavaScript. app.js already delegates [data-toggle], and hx-trigger="intersect once" makes the load lazy for free: a hidden element never intersects, so the request fires the first time it is opened and never on a page load nobody looked at. Image data URIs are replaced before dumping -- fidelity is the point, but not several megabytes of base64 in the DOM. Everything renders through normal escaping and never |safe: this JSON is full of model output, search results and uploaded documents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
949 lines
26 KiB
CSS
949 lines
26 KiB
CSS
/*
|
|
Application styles.
|
|
|
|
Rules here resolve colour, spacing and radius through the variables in
|
|
tokens.css and never hard-code a value. Layout is flexbox and grid only --
|
|
no framework, no preprocessor, no build step.
|
|
*/
|
|
|
|
/* --- Reset ---------------------------------------------------------------- */
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
}
|
|
|
|
/*
|
|
The `hidden` attribute has to win.
|
|
|
|
The browser's own rule is `[hidden] { display: none }`, which any component
|
|
rule setting `display` outranks -- `.btn` is `display: inline-flex`, so a
|
|
button hidden from JavaScript stayed visible. That is not a styling nit: it
|
|
is how the Stop button came to sit permanently beside Send. Anything toggled
|
|
with `hidden` anywhere in the application depends on this line.
|
|
*/
|
|
[hidden] {
|
|
display: none !important;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
font-family: var(--font-body);
|
|
font-size: var(--text-base);
|
|
line-height: var(--leading-normal);
|
|
color: var(--ink);
|
|
background: var(--bg);
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
h1, h2, h3, h4, h5, h6 {
|
|
margin: 0;
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
line-height: var(--leading-tight);
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
p { margin: 0 0 var(--sp-4); }
|
|
p:last-child { margin-bottom: 0; }
|
|
|
|
a {
|
|
color: var(--accent);
|
|
text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
|
|
text-underline-offset: 2px;
|
|
}
|
|
a:hover { color: var(--accent-hover); }
|
|
|
|
button, input, textarea, select {
|
|
font: inherit;
|
|
color: inherit;
|
|
}
|
|
|
|
/* A single, consistent focus ring. Never remove it without a replacement. */
|
|
:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
:focus:not(:focus-visible) { outline: none; }
|
|
|
|
.visually-hidden {
|
|
position: absolute;
|
|
width: 1px; height: 1px;
|
|
padding: 0; margin: -1px;
|
|
overflow: hidden;
|
|
clip-path: inset(50%);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* --- Icons ---------------------------------------------------------------- */
|
|
.icon {
|
|
width: 1.25em;
|
|
height: 1.25em;
|
|
flex: none;
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 1.7;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
.icon--sm { width: 1em; height: 1em; }
|
|
.icon--lg { width: 1.5em; height: 1.5em; }
|
|
/* The leaf is a filled silhouette, not a stroked pictogram. */
|
|
.icon--leaf { fill: currentColor; stroke: none; }
|
|
|
|
/* --- Buttons ---------------------------------------------------------------
|
|
Every variant is the same height and vertically centres its contents, so a
|
|
row of mixed buttons lines up without per-instance nudging. Icon buttons are
|
|
square at that height rather than a different shape.
|
|
*/
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--sp-2);
|
|
height: var(--control-h);
|
|
padding: 0 var(--control-px);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface-raised);
|
|
color: var(--ink);
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
text-decoration: none;
|
|
transition: background var(--transition-fast), border-color var(--transition-fast),
|
|
color var(--transition-fast);
|
|
}
|
|
.btn:hover:not(:disabled) {
|
|
background: var(--surface-hover);
|
|
border-color: var(--border-strong);
|
|
color: var(--ink);
|
|
}
|
|
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
|
|
.btn--primary {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: var(--accent-ink);
|
|
}
|
|
.btn--primary:hover:not(:disabled) {
|
|
background: var(--accent-hover);
|
|
border-color: var(--accent-hover);
|
|
color: var(--accent-ink);
|
|
}
|
|
|
|
.btn--danger { color: var(--danger); }
|
|
.btn--danger:hover:not(:disabled) {
|
|
background: var(--danger-soft);
|
|
border-color: var(--danger);
|
|
color: var(--danger);
|
|
}
|
|
|
|
.btn--ghost { background: transparent; border-color: transparent; }
|
|
.btn--ghost:hover:not(:disabled) { background: var(--surface-hover); border-color: transparent; }
|
|
|
|
/* Square, and the same height as everything beside it. */
|
|
.btn--icon {
|
|
width: var(--control-h);
|
|
padding: 0;
|
|
background: transparent;
|
|
border-color: transparent;
|
|
color: var(--ink-muted);
|
|
}
|
|
.btn--icon:hover:not(:disabled) { background: var(--surface-hover); color: var(--ink); }
|
|
.btn--icon.btn--primary {
|
|
color: var(--accent-ink);
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.btn--sm { height: var(--control-h-sm); padding: 0 var(--control-px-sm); font-size: var(--text-xs); }
|
|
.btn--sm.btn--icon { width: var(--control-h-sm); padding: 0; }
|
|
.btn--lg { height: var(--control-h-lg); padding: 0 var(--sp-5); font-size: var(--text-base); }
|
|
|
|
.btn--block { width: 100%; }
|
|
.btn--grow { flex: 1; }
|
|
|
|
/* Rows of buttons: gap and alignment in one place, not per instance. */
|
|
.btn-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
|
|
.btn-row--end { justify-content: flex-end; }
|
|
.spacer { flex: 1; }
|
|
|
|
/* --- Forms -----------------------------------------------------------------
|
|
Inputs share the button height, so a control row is flush by construction.
|
|
*/
|
|
.field { margin-bottom: var(--sp-4); }
|
|
.field:last-child { margin-bottom: 0; }
|
|
|
|
.field__label {
|
|
display: block;
|
|
margin-bottom: var(--sp-2);
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
color: var(--ink);
|
|
}
|
|
.field__hint {
|
|
margin-top: var(--sp-2);
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
line-height: var(--leading-normal);
|
|
}
|
|
|
|
.input,
|
|
.select {
|
|
width: 100%;
|
|
height: var(--control-h);
|
|
padding: 0 var(--control-px);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--bg-sunken);
|
|
color: var(--ink);
|
|
font-size: var(--text-sm);
|
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
|
}
|
|
.textarea {
|
|
width: 100%;
|
|
padding: var(--sp-2) var(--control-px);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--bg-sunken);
|
|
color: var(--ink);
|
|
font-size: var(--text-sm);
|
|
line-height: var(--leading-normal);
|
|
resize: vertical;
|
|
min-height: 5rem;
|
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
|
}
|
|
|
|
.input:focus,
|
|
.textarea:focus,
|
|
.select:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
}
|
|
.input::placeholder, .textarea::placeholder { color: var(--ink-faint); }
|
|
|
|
/*
|
|
File inputs.
|
|
|
|
A file input is two things in one box -- a button the browser draws and the
|
|
chosen filename beside it -- and neither inherits anything useful. Left alone
|
|
with `.input`, the padding applies to the whole control so the button sits
|
|
hard against the left edge while the text floats off its centre line.
|
|
|
|
So: no horizontal padding on the control, the button styled to the same
|
|
height as everything else and given the right border that separates it, and
|
|
the filename centred with line-height rather than flexbox, which file inputs
|
|
do not lay out reliably.
|
|
*/
|
|
.input[type="file"] {
|
|
padding: 0 var(--control-px) 0 0;
|
|
line-height: calc(var(--control-h) - 2px);
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--ink-muted);
|
|
}
|
|
.input[type="file"]::file-selector-button {
|
|
height: calc(var(--control-h) - 2px);
|
|
margin: 0 var(--sp-3) 0 0;
|
|
padding: 0 var(--control-px);
|
|
border: 0;
|
|
border-right: 1px solid var(--border);
|
|
background: var(--surface-hover);
|
|
color: var(--ink);
|
|
font: inherit;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background var(--transition-fast);
|
|
}
|
|
.input[type="file"]:hover::file-selector-button { background: var(--surface-active); }
|
|
.input--mono, .textarea--mono { font-family: var(--font-mono); font-size: var(--text-xs); }
|
|
|
|
.select {
|
|
appearance: none;
|
|
padding-right: var(--sp-8);
|
|
/* Chevron drawn in CSS, so no icon font and no extra element. */
|
|
background-image:
|
|
linear-gradient(45deg, transparent 50%, currentColor 50%),
|
|
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
|
background-position: right 1.1rem center, right 0.85rem center;
|
|
background-size: 0.3rem 0.3rem, 0.3rem 0.3rem;
|
|
background-repeat: no-repeat;
|
|
cursor: pointer;
|
|
}
|
|
/* Sits inside a composed control that draws its own frame (the model picker). */
|
|
.select--bare {
|
|
border-color: transparent;
|
|
background-color: transparent;
|
|
width: auto;
|
|
max-width: 14rem;
|
|
padding-left: var(--sp-1);
|
|
}
|
|
.select--bare:focus { box-shadow: none; }
|
|
|
|
.checkbox {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
font-size: var(--text-sm);
|
|
cursor: pointer;
|
|
line-height: var(--leading-normal);
|
|
}
|
|
.checkbox input {
|
|
accent-color: var(--accent);
|
|
width: 1rem;
|
|
height: 1rem;
|
|
flex: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Multi-column form layout, one definition. */
|
|
.grid { display: grid; gap: var(--sp-4); }
|
|
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); }
|
|
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
|
|
|
|
/* --- Alerts --------------------------------------------------------------- */
|
|
.alert {
|
|
display: flex;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-3) var(--sp-4);
|
|
border: 1px solid var(--border);
|
|
border-left-width: 3px;
|
|
border-radius: var(--radius);
|
|
font-size: var(--text-sm);
|
|
margin-bottom: var(--sp-4);
|
|
background: var(--surface);
|
|
}
|
|
.alert--error { border-left-color: var(--danger); background: var(--danger-soft); color: var(--ink); }
|
|
.alert--success { border-left-color: var(--success); background: var(--success-soft); }
|
|
.alert--warning { border-left-color: var(--warning); background: var(--warning-soft); }
|
|
.alert__icon { color: var(--danger); flex: none; margin-top: 0.15rem; }
|
|
|
|
/* --- Badges --------------------------------------------------------------- */
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
padding: 0.1rem 0.45rem;
|
|
border-radius: var(--radius-full);
|
|
font-size: var(--text-xs);
|
|
font-weight: 500;
|
|
background: var(--surface-active);
|
|
color: var(--ink-muted);
|
|
}
|
|
.badge--leaf { background: var(--leaf-soft); color: var(--leaf); }
|
|
.badge--success { background: var(--success-soft); color: var(--success); }
|
|
.badge--danger { background: var(--danger-soft); color: var(--danger); }
|
|
.badge--warning { background: var(--warning-soft); color: var(--warning); }
|
|
|
|
/* --- Application shell ----------------------------------------------------- */
|
|
.shell { display: flex; height: 100dvh; overflow: hidden; }
|
|
|
|
.sidebar {
|
|
width: var(--sidebar-width);
|
|
flex: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-sunken);
|
|
border-right: 1px solid var(--border);
|
|
min-height: 0;
|
|
}
|
|
.sidebar[hidden] { display: none; }
|
|
|
|
.sidebar__header {
|
|
display: flex;
|
|
align-items: center;
|
|
height: var(--header-height);
|
|
padding: 0 var(--sp-3);
|
|
flex: none;
|
|
}
|
|
.sidebar__brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
color: var(--ink);
|
|
text-decoration: none;
|
|
font-family: var(--font-display);
|
|
font-size: var(--text-lg);
|
|
font-weight: 600;
|
|
letter-spacing: 0.01em;
|
|
min-width: 0;
|
|
}
|
|
.sidebar__brand:hover { color: var(--ink); }
|
|
.sidebar__brand .brand-mark { width: 1.65rem; height: 1.65rem; flex: none; }
|
|
.sidebar__brand .brand-llm { color: var(--leaf); }
|
|
|
|
.sidebar__actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
padding: 0 var(--sp-3) var(--sp-3);
|
|
flex: none;
|
|
}
|
|
|
|
.sidebar__scroll {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
padding: 0 var(--sp-2) var(--sp-3);
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--border-strong) transparent;
|
|
}
|
|
|
|
.sidebar__footer {
|
|
flex: none;
|
|
border-top: 1px solid var(--border);
|
|
padding: var(--sp-2);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-1);
|
|
}
|
|
.sidebar__tools { display: flex; align-items: center; gap: var(--sp-1); }
|
|
|
|
.main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
background: var(--bg);
|
|
}
|
|
|
|
/* The inspector, mirroring the sidebar on the other side of .shell. Hidden by
|
|
the `hidden` attribute, which the rule at the top of this file forces to win. */
|
|
.inspector {
|
|
width: var(--inspector-width);
|
|
flex: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
background: var(--bg-sunken);
|
|
border-left: 1px solid var(--border);
|
|
}
|
|
|
|
.inspector__header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
height: var(--header-height);
|
|
flex: none;
|
|
padding: 0 var(--sp-3);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.inspector__title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
flex: 1;
|
|
font-size: var(--text-sm);
|
|
font-weight: 600;
|
|
color: var(--ink-muted);
|
|
}
|
|
|
|
.inspector__body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
padding: var(--sp-4);
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--border-strong) transparent;
|
|
}
|
|
|
|
.inspector__heading {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
font-size: var(--text-sm);
|
|
margin: var(--sp-5) 0 var(--sp-2);
|
|
}
|
|
.inspector__note {
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-muted);
|
|
line-height: var(--leading-normal);
|
|
margin: 0 0 var(--sp-3);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.inspector__facts {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: var(--sp-1) var(--sp-3);
|
|
font-size: var(--text-xs);
|
|
margin: 0;
|
|
}
|
|
.inspector__facts dt { color: var(--ink-faint); }
|
|
.inspector__facts dd { margin: 0; overflow-wrap: anywhere; }
|
|
|
|
.inspector__json {
|
|
margin: 0 0 var(--sp-3);
|
|
padding: var(--sp-2);
|
|
max-height: 22rem;
|
|
overflow: auto;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
background: var(--code-bg);
|
|
border: 1px solid var(--code-border);
|
|
border-radius: var(--radius);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
line-height: var(--leading-normal);
|
|
color: var(--ink-muted);
|
|
}
|
|
|
|
@media (max-width: 64rem) {
|
|
.inspector {
|
|
position: fixed;
|
|
inset: 0 0 0 auto;
|
|
z-index: 40;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
}
|
|
|
|
.topbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
height: var(--header-height);
|
|
flex: none;
|
|
padding: 0 var(--sp-4);
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg);
|
|
}
|
|
.topbar__title {
|
|
font-family: var(--font-display);
|
|
font-size: var(--text-md);
|
|
font-weight: 600;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.topbar__actions { display: flex; align-items: center; gap: var(--sp-2); flex: none; }
|
|
|
|
/* The model picker: an avatar and a select sharing one frame, so it reads as a
|
|
single control rather than two things that happen to be adjacent. */
|
|
.model-select {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
height: var(--control-h);
|
|
padding: 0 var(--sp-1) 0 var(--sp-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
cursor: pointer;
|
|
transition: border-color var(--transition-fast);
|
|
}
|
|
.model-select:hover { border-color: var(--border-strong); }
|
|
.model-select:focus-within {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
}
|
|
.model-select__avatar { width: 1.4rem; height: 1.4rem; border-radius: var(--radius-sm); }
|
|
|
|
/* Collapsible settings panel, shared by chat settings and anything like it. */
|
|
.panel {
|
|
flex: none;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-sunken);
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
}
|
|
.panel__inner {
|
|
max-width: var(--thread-max-width);
|
|
margin: 0 auto;
|
|
padding: var(--sp-5);
|
|
}
|
|
.panel__note {
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
font-style: italic;
|
|
margin-bottom: var(--sp-4);
|
|
}
|
|
|
|
/* --- Sidebar navigation ---------------------------------------------------- */
|
|
.nav-group { margin-bottom: var(--sp-4); }
|
|
.nav-group:last-child { margin-bottom: 0; }
|
|
.nav-group__label {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--sp-2) var(--sp-2) var(--sp-1);
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--ink-faint);
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
min-height: var(--control-h);
|
|
padding: 0 var(--sp-2);
|
|
border-radius: var(--radius);
|
|
color: var(--ink-muted);
|
|
text-decoration: none;
|
|
font-size: var(--text-sm);
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
.nav-item:hover { background: var(--surface-hover); color: var(--ink); }
|
|
.nav-item.is-active {
|
|
background: var(--surface-active);
|
|
color: var(--ink);
|
|
font-weight: 500;
|
|
}
|
|
.nav-item__label {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.nav-item__avatar { width: 1.4rem; height: 1.4rem; border-radius: var(--radius-sm); flex: none; }
|
|
.nav-item--model .nav-item__label { font-weight: 500; }
|
|
|
|
/* Row actions stay hidden until hover or focus, so the list reads calmly while
|
|
remaining keyboard reachable. */
|
|
.nav-item__actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.1rem;
|
|
flex: none;
|
|
opacity: 0;
|
|
transition: opacity var(--transition-fast);
|
|
}
|
|
.nav-item:hover .nav-item__actions,
|
|
.nav-item:focus-within .nav-item__actions { opacity: 1; }
|
|
|
|
.nav-empty {
|
|
padding: var(--sp-2);
|
|
margin: 0;
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
font-style: italic;
|
|
}
|
|
|
|
/* --- Auth screens --------------------------------------------------------- */
|
|
.auth {
|
|
min-height: 100dvh;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: var(--sp-6);
|
|
background:
|
|
radial-gradient(ellipse 60% 50% at 50% 0%, var(--leaf-soft), transparent 70%),
|
|
var(--bg);
|
|
}
|
|
.auth__card {
|
|
width: 100%;
|
|
max-width: 25rem;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-xl);
|
|
padding: var(--sp-8);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
.auth__brand { display: grid; place-items: center; gap: var(--sp-3); margin-bottom: var(--sp-6); }
|
|
.auth__brand .brand-mark { width: 3.5rem; height: 3.5rem; }
|
|
.auth__title {
|
|
font-size: var(--text-2xl);
|
|
font-family: var(--font-display);
|
|
text-align: center;
|
|
}
|
|
.auth__subtitle {
|
|
text-align: center;
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
margin-top: var(--sp-2);
|
|
}
|
|
.auth__footer {
|
|
margin-top: var(--sp-5);
|
|
padding-top: var(--sp-4);
|
|
border-top: 1px solid var(--border);
|
|
text-align: center;
|
|
font-size: var(--text-sm);
|
|
color: var(--ink-muted);
|
|
}
|
|
|
|
/* --- Empty states --------------------------------------------------------- */
|
|
.empty {
|
|
flex: 1;
|
|
display: grid;
|
|
place-content: center;
|
|
justify-items: center;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-10);
|
|
text-align: center;
|
|
}
|
|
.empty__mark { width: 4.5rem; height: 4.5rem; opacity: 0.85; }
|
|
.empty__title { font-size: var(--text-xl); font-family: var(--font-display); }
|
|
.empty__text {
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
max-width: 32rem;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* --- Utilities ------------------------------------------------------------ */
|
|
.stack > * + * { margin-top: var(--sp-4); }
|
|
.row { display: flex; align-items: center; gap: var(--sp-3); }
|
|
.row--between { justify-content: space-between; }
|
|
.muted { color: var(--ink-muted); }
|
|
.faint { color: var(--ink-faint); }
|
|
.text-sm { font-size: var(--text-sm); }
|
|
.text-xs { font-size: var(--text-xs); }
|
|
.mono { font-family: var(--font-mono); }
|
|
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
/* --- Small screens -------------------------------------------------------- */
|
|
@media (max-width: 48rem) {
|
|
.sidebar {
|
|
position: fixed;
|
|
inset: 0 auto 0 0;
|
|
z-index: 40;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
/* Hiding it is the `hidden` attribute, forced to win at the top of this
|
|
file. There used to be a `[data-collapsed="true"]` rule here that nothing
|
|
ever set. */
|
|
}
|
|
|
|
/* --- Toasts ----------------------------------------------------------------
|
|
Bottom-right, stacked, above dialogs' backdrop but out of the way of the
|
|
composer.
|
|
*/
|
|
.toasts {
|
|
position: fixed;
|
|
right: var(--sp-4);
|
|
bottom: var(--sp-4);
|
|
z-index: 60;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-2);
|
|
max-width: min(24rem, calc(100vw - var(--sp-8)));
|
|
pointer-events: none;
|
|
}
|
|
|
|
.toast {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-4);
|
|
border: 1px solid var(--border);
|
|
border-left: 3px solid var(--accent);
|
|
border-radius: var(--radius);
|
|
background: var(--surface-raised);
|
|
box-shadow: var(--shadow-lg);
|
|
font-size: var(--text-sm);
|
|
pointer-events: auto;
|
|
opacity: 0;
|
|
transform: translateY(0.5rem);
|
|
transition: opacity var(--transition), transform var(--transition);
|
|
}
|
|
.toast.is-in { opacity: 1; transform: none; }
|
|
.toast--success { border-left-color: var(--success); }
|
|
.toast--error { border-left-color: var(--danger); }
|
|
.toast--warning { border-left-color: var(--warning); }
|
|
.toast__text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
.toast__close {
|
|
flex: none;
|
|
border: 0;
|
|
background: none;
|
|
color: var(--ink-faint);
|
|
cursor: pointer;
|
|
font-size: var(--text-lg);
|
|
line-height: 1;
|
|
padding: 0 0.15rem;
|
|
}
|
|
.toast__close:hover { color: var(--ink); }
|
|
|
|
/* --- Dialogs ----------------------------------------------------------------
|
|
<dialog> gives focus trapping, Escape and page inertness for free.
|
|
*/
|
|
.dialog {
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-xl);
|
|
background: var(--surface);
|
|
color: var(--ink);
|
|
padding: 0;
|
|
max-width: min(28rem, calc(100vw - var(--sp-8)));
|
|
width: 100%;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
.dialog::backdrop { background: var(--scrim); backdrop-filter: blur(2px); }
|
|
|
|
.dialog__form { padding: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-3); }
|
|
.dialog__title { font-size: var(--text-lg); }
|
|
.dialog__message {
|
|
margin: 0;
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
line-height: var(--leading-relaxed);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.dialog__actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: var(--sp-2);
|
|
margin-top: var(--sp-2);
|
|
}
|
|
|
|
/* A destructive confirmation needs the weight of a filled button, not the
|
|
outline treatment .btn--danger gives a row action. */
|
|
.btn--danger-solid {
|
|
background: var(--danger);
|
|
border-color: var(--danger);
|
|
color: var(--ink-inverse);
|
|
}
|
|
.btn--danger-solid:hover:not(:disabled) {
|
|
background: var(--danger-hover);
|
|
border-color: var(--danger-hover);
|
|
color: var(--ink-inverse);
|
|
}
|
|
|
|
/* --- Model picker ----------------------------------------------------------
|
|
Built by hand because a <select> renders only text in an <option> -- no
|
|
avatar, no description, no badges.
|
|
*/
|
|
.picker { position: relative; }
|
|
|
|
.picker__button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
height: var(--control-h);
|
|
max-width: 16rem;
|
|
padding: 0 var(--sp-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--surface);
|
|
color: var(--ink);
|
|
font-size: var(--text-sm);
|
|
cursor: pointer;
|
|
transition: border-color var(--transition-fast);
|
|
}
|
|
.picker__button:hover { border-color: var(--border-strong); }
|
|
.picker__button[aria-expanded="true"] {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
}
|
|
.picker__avatar { width: 1.4rem; height: 1.4rem; border-radius: var(--radius-sm); flex: none; }
|
|
.picker__label {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
min-width: 0;
|
|
font-weight: 500;
|
|
}
|
|
.picker__chevron { flex: none; color: var(--ink-faint); }
|
|
|
|
.picker__menu {
|
|
position: absolute;
|
|
top: calc(100% + var(--sp-1));
|
|
right: 0;
|
|
z-index: 30;
|
|
width: min(24rem, calc(100vw - var(--sp-8)));
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
background: var(--surface-raised);
|
|
box-shadow: var(--shadow-lg);
|
|
overflow: hidden;
|
|
}
|
|
/* The composer's attach menu opens upwards: it sits at the bottom of the
|
|
window, so a menu dropping down would be off screen. */
|
|
.picker--up .picker__menu {
|
|
top: auto;
|
|
bottom: calc(100% + var(--sp-1));
|
|
left: 0;
|
|
right: auto;
|
|
}
|
|
.picker__menu--compact { width: min(18rem, calc(100vw - var(--sp-8))); padding: var(--sp-1); }
|
|
.picker__option-note {
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* A dialog that holds a searchable list rather than a question. */
|
|
.dialog--wide { width: min(34rem, calc(100vw - var(--sp-6))); }
|
|
.dialog__results {
|
|
max-height: min(24rem, 50vh);
|
|
overflow-y: auto;
|
|
scrollbar-width: thin;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
}
|
|
.dialog__results .picker__list { max-height: none; }
|
|
|
|
.picker__search { padding: var(--sp-2); border-bottom: 1px solid var(--border); }
|
|
.input--sm { height: var(--control-h-sm); font-size: var(--text-xs); }
|
|
.picker__list { max-height: 22rem; overflow-y: auto; scrollbar-width: thin; padding: var(--sp-1); }
|
|
|
|
.picker__option {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--sp-3);
|
|
width: 100%;
|
|
padding: var(--sp-2);
|
|
border: 0;
|
|
border-radius: var(--radius);
|
|
background: none;
|
|
color: var(--ink);
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.picker__option:hover, .picker__option:focus-visible { background: var(--surface-hover); }
|
|
.picker__option.is-selected { background: var(--accent-soft); }
|
|
.picker__option .picker__avatar { width: 1.75rem; height: 1.75rem; margin-top: 0.1rem; }
|
|
.picker__option-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
|
|
.picker__option-name {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
}
|
|
.picker__pin { color: var(--leaf); }
|
|
.picker__option-desc {
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
line-height: var(--leading-normal);
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.picker__option-tags { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
|
|
.picker__option-tags:empty { display: none; }
|
|
.tag {
|
|
font-size: 0.6875rem;
|
|
padding: 0 0.35rem;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--leaf-soft);
|
|
color: var(--leaf);
|
|
}
|
|
.picker__tick { color: var(--accent); flex: none; margin-top: 0.35rem; }
|
|
.picker__empty {
|
|
padding: var(--sp-4);
|
|
margin: 0;
|
|
text-align: center;
|
|
font-size: var(--text-sm);
|
|
color: var(--ink-faint);
|
|
}
|