Files
LocalAI/core/http/react-ui/src/App.css
T
gangna2s-arch b4868d0941 fix(ui): keep the language switcher menu inside the viewport
The menu opens upwards from the sidebar footer and is a child of the
scrollable sidebar. On short windows it was taller than the space above
the trigger, so its top (the first entries) was clipped by the sidebar.

Cap the menu height to the viewport and let it scroll instead.

Signed-off-by: gangna2s-arch <281744444+gangna2s-arch@users.noreply.github.com>
(cherry picked from commit 9a564ef79d)
2026-10-25 08:43:05 +00:00

9711 lines
273 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Layout */
.app-layout {
display: flex;
min-height: 100vh;
min-height: 100dvh;
background-color: var(--color-bg-primary);
}
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
/* A flex item's automatic minimum is its content's minimum, so without this
any single wide descendant (a long install error in the operations strip,
a wide table on a phone) drags the whole column past the viewport and
every page gets a horizontal scrollbar. */
min-width: 0;
transition: margin-left var(--duration-normal) var(--ease-default);
}
.sidebar-is-collapsed .main-content {
margin-left: var(--sidebar-width-collapsed);
}
.main-content-inner {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.app-layout-chat {
height: 100vh;
height: 100dvh;
}
.app-layout-chat .main-content {
height: 100vh;
height: 100dvh;
min-height: 0;
min-width: 0;
overflow: hidden;
}
.app-layout-chat .main-content-inner {
overflow: hidden;
min-width: 0;
}
/* Footer */
/* One line, not three stacked centred rows. The footer is chrome: it should
cost a line of height and be findable, not occupy the bottom sixth of every
page. Version left, links right, copyright folded in beside the version. */
.app-footer {
background: transparent;
border-top: 1px solid var(--color-border-divider);
padding: var(--spacing-sm) var(--spacing-lg);
margin-top: auto;
}
.app-footer-inner {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--spacing-xs) var(--spacing-md);
}
/* Links push right on a wide viewport and wrap under on a narrow one. */
.app-footer-links { margin-left: auto; }
/* Below the fold of a phone the three groups stack anyway; centre them so the
wrap does not read as a ragged left column. */
@media (max-width: 560px) {
.app-footer-inner { justify-content: center; }
.app-footer-links { margin-left: 0; }
}
.app-footer-version {
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.app-footer-version a {
color: var(--color-text-muted);
text-decoration: none;
transition: color 150ms;
}
.app-footer-version a:hover {
color: var(--color-text-secondary);
}
.app-footer-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--spacing-md);
}
.app-footer-links a {
display: flex;
align-items: center;
gap: 4px;
font-size: var(--text-xs);
color: var(--color-text-muted);
text-decoration: none;
transition: color 150ms;
}
.app-footer-links a:hover {
color: var(--color-text-secondary);
}
.app-footer-copyright {
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.app-footer-copyright a {
color: var(--color-text-muted);
text-decoration: none;
transition: color 150ms;
}
.app-footer-copyright a:hover {
color: var(--color-text-secondary);
}
/* Mobile header */
.mobile-header {
display: none;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-bg-primary);
border-bottom: 1px solid var(--color-border-subtle);
}
.hamburger-btn {
background: none;
border: none;
color: var(--color-text-primary);
font-size: 1.25rem;
cursor: pointer;
padding: var(--spacing-xs);
border-radius: var(--radius-md);
transition: background var(--dk-dur-1) var(--dk-ease-out),
transform var(--dk-dur-1) var(--dk-ease-out);
}
.hamburger-btn:hover { background: var(--color-bg-hover); }
.hamburger-btn:active { transform: scale(var(--dk-press-scale)); }
.hamburger-btn:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.mobile-title {
font-weight: 600;
color: var(--color-text-primary);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mobile-header-actions {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
.mobile-header-btn {
background: none;
border: none;
color: var(--color-text-primary);
font-size: 1.05rem;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
padding: 10px;
border-radius: var(--radius-full);
transition: background var(--dk-dur-1) var(--dk-ease-out),
transform var(--dk-dur-1) var(--dk-ease-out);
}
.mobile-header-btn:hover {
background: var(--color-bg-hover);
}
.mobile-header-btn:active {
transform: scale(var(--dk-press-scale));
}
.mobile-header-btn:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.mobile-header-avatar {
padding: 4px;
}
.mobile-header-avatar img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
display: block;
}
.mobile-header-avatar .lai-icon {
font-size: 1.5rem;
}
/* Sidebar */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: var(--sidebar-width);
height: 100vh;
height: 100dvh;
background: var(--color-bg-primary);
border-right: 1px solid transparent;
display: flex;
flex-direction: column;
z-index: 50;
overflow-y: auto;
box-shadow: var(--shadow-sidebar);
transition: width var(--duration-normal) var(--ease-spring),
transform var(--duration-normal) var(--ease-spring);
will-change: transform;
}
.sidebar-overlay {
display: none;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-sm) var(--spacing-md);
min-height: 72px;
}
.sidebar-logo-link {
display: flex;
align-items: center;
min-width: 0;
/* Line the mark up with the nav icons: row margin plus row padding. */
padding-left: var(--spacing-sm);
border-radius: var(--radius-md);
}
.sidebar-logo-img {
display: block;
width: 136px;
max-width: 100%;
height: auto;
max-height: 64px;
object-fit: contain;
}
.sidebar-logo-icon {
display: none;
}
.sidebar-logo-icon-img {
display: block;
width: 36px;
height: 36px;
object-fit: contain;
}
.sidebar-close-btn {
display: none;
background: none;
border: none;
color: var(--color-text-secondary);
font-size: 1.25rem;
cursor: pointer;
border-radius: var(--radius-md);
transition: background var(--dk-dur-1) var(--dk-ease-out),
transform var(--dk-dur-1) var(--dk-ease-out);
}
.sidebar-close-btn:hover { background: var(--color-bg-hover); }
.sidebar-close-btn:active { transform: scale(var(--dk-press-scale)); }
.sidebar-close-btn:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.sidebar-nav {
flex: 1;
padding: var(--spacing-xs) 0;
overflow-y: auto;
}
.sidebar-section {
padding: var(--spacing-xs) 0;
}
.sidebar-section-title {
padding: var(--spacing-md) var(--spacing-sm) var(--spacing-xs);
font-size: var(--text-xs);
font-weight: var(--font-weight-bold);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--color-text-muted);
white-space: nowrap;
overflow: hidden;
}
.sidebar-section-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
transition: color var(--dk-dur-1) var(--dk-ease-out);
}
.sidebar-section-toggle:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
border-radius: var(--radius-md);
}
.sidebar-section-toggle:hover {
color: var(--color-text-primary);
}
.sidebar-section-chevron {
font-size: 0.5rem;
opacity: 0.6;
transition: transform var(--dk-dur-2) var(--dk-ease-out), opacity var(--dk-dur-1);
flex-shrink: 0;
}
.sidebar-section-toggle:hover .sidebar-section-chevron {
opacity: 1;
}
.sidebar-section-toggle.open .sidebar-section-chevron {
transform: rotate(90deg);
}
.nav-item {
position: relative;
display: flex;
align-items: center;
gap: var(--spacing-sm);
min-height: var(--control-h);
padding: 8px var(--spacing-md) 8px calc(var(--spacing-sm) + 2px);
color: var(--color-text-secondary);
text-decoration: none;
font-size: var(--text-base);
font-weight: var(--font-weight-medium);
transition: color var(--dk-dur-1) var(--dk-ease-out),
background var(--dk-dur-1) var(--dk-ease-out),
box-shadow var(--dk-dur-1) var(--dk-ease-out),
transform var(--dk-dur-1) var(--dk-ease-spring);
white-space: nowrap;
overflow: hidden;
}
.nav-item:hover:not(.active) {
color: var(--color-text-primary);
background: var(--color-bg-hover);
}
.nav-item:active:not(.disabled) {
transform: scale(var(--dk-press-scale-nav));
}
.nav-item:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
/* Inside a card (the Operate rail) the current row takes the accent wash.
In the main sidebar it lifts onto a card instead; see .sidebar-nav. */
.nav-item.active {
color: var(--dk-accent-text);
background: var(--color-primary-light);
font-weight: var(--font-weight-medium);
}
.nav-icon {
width: 20px;
text-align: center;
flex-shrink: 0;
font-size: 1rem;
}
.nav-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 150ms ease;
}
/* Sidebar nav polish: inset, rounded "pill" items give the nav a lighter,
more intentional rhythm than full-bleed bars. Scoped to .sidebar-nav so the
shared .nav-item class keeps its own treatment. */
.sidebar-nav .nav-item {
margin: 2px var(--spacing-sm);
gap: var(--spacing-sm);
/* 8px margin + 8px padding + half the 20px icon puts the icon centre 26px
in. The footer controls use the same figure (see .sidebar-footer). */
padding-left: var(--spacing-sm);
padding-right: var(--spacing-sm);
border-radius: var(--radius-md);
}
/* The current row lifts onto a card with the rest shadow. That lift is the
whole indicator; the icon keeps the accent. */
.sidebar-nav .nav-item.active {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
font-weight: var(--font-weight-semibold);
box-shadow: var(--shadow-sm);
}
.sidebar-nav .nav-item.active .nav-icon { color: var(--dk-accent-text); }
/* A dot on the current row, as the kit shows it. It is decoration: the row
already carries aria-current from the router link. */
.sidebar-nav .nav-item.active::after {
content: '';
flex: none;
width: 6px;
height: 6px;
margin-left: var(--spacing-xs);
border-radius: var(--radius-full);
background: var(--dk-accent);
}
.sidebar.collapsed .sidebar-nav .nav-item.active::after { display: none; }
/* Align tier labels with the inset item text (item margin + icon padding). */
.sidebar-nav .sidebar-section-title {
padding-left: calc(var(--spacing-sm) + var(--spacing-sm));
padding-right: calc(var(--spacing-sm) + var(--spacing-sm));
}
.nav-external {
font-size: 0.55rem;
margin-left: auto;
opacity: 0.5;
flex-shrink: 0;
}
.sidebar-footer {
/* 6px inline padding plus the 40px control boxes puts each icon centre at
26px, on the nav icon column. */
padding: var(--spacing-sm) 6px;
border-top: 1px solid var(--color-border-divider);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-xs);
flex-wrap: wrap;
}
.sidebar-user {
display: flex;
align-items: center;
gap: var(--spacing-xs);
width: 100%;
padding: var(--spacing-xs) 0;
font-size: var(--text-sm);
color: var(--color-text-secondary);
overflow: hidden;
}
.sidebar-user-avatar {
width: 28px;
height: 28px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.sidebar-user-avatar-icon {
/* Exactly the 28px box the old glyph filled, so the avatar centre stays on
the nav icon column. */
width: 28px;
height: 28px;
font-size: 1.75rem;
color: var(--color-text-muted);
flex-shrink: 0;
}
.sidebar-user-link {
display: flex;
align-items: center;
gap: var(--spacing-xs);
flex: 1;
min-width: 0;
min-height: var(--control-h);
background: none;
border: none;
padding: 0 6px;
margin: 0;
border-radius: var(--radius-md);
color: inherit;
font: inherit;
cursor: pointer;
transition: background var(--dk-dur-1) var(--dk-ease-out), color var(--dk-dur-1) var(--dk-ease-out);
}
.sidebar-user-link:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.sidebar-user-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.sidebar-logout-btn {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: 2px 4px;
border-radius: var(--radius-sm);
font-size: 0.75rem;
flex-shrink: 0;
transition: color var(--duration-fast);
}
.sidebar-logout-btn:hover {
color: var(--color-error);
}
.sidebar.collapsed .sidebar-user {
justify-content: center;
}
.sidebar.collapsed .sidebar-user-link {
/* Not "flex: 0": its basis of 0% and min-width: 0 shrink the link to its
padding, and the avatar then overflows to the right of the link's left
edge instead of sitting centred in it. */
flex: 0 0 auto;
margin: 0;
padding: 0;
}
.sidebar.collapsed .sidebar-user-name,
.sidebar.collapsed .sidebar-logout-btn {
display: none;
}
.sidebar-collapse-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--control-h);
height: var(--control-h);
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
border-radius: var(--radius-md);
font-size: var(--text-sm);
transition: color var(--dk-dur-1) var(--dk-ease-out), background var(--dk-dur-1) var(--dk-ease-out),
transform var(--dk-dur-1) var(--dk-ease-out);
flex-shrink: 0;
}
.sidebar-collapse-btn:hover {
color: var(--color-text-primary);
background: var(--color-surface-hover);
}
.sidebar-collapse-btn:active { transform: scale(var(--dk-press-scale)); }
.sidebar-collapse-btn:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
/* Collapsed sidebar (desktop only) */
.sidebar.collapsed {
width: var(--sidebar-width-collapsed);
}
.sidebar.collapsed .sidebar-logo-link {
display: none;
}
.sidebar.collapsed .sidebar-logo-icon {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.sidebar.collapsed .sidebar-header {
justify-content: center;
}
.sidebar.collapsed .nav-label,
.sidebar.collapsed .nav-external,
.sidebar.collapsed .sidebar-section-title {
display: none;
}
.sidebar.collapsed .sidebar-section-chevron {
display: none;
}
.sidebar.collapsed .nav-item {
justify-content: center;
padding: 8px 0;
border-left-width: 2px;
/* A 40px square tile centred in the rail. */
margin-inline: calc((var(--sidebar-width-collapsed) - var(--control-h)) / 2);
}
.sidebar.collapsed .nav-icon {
width: auto;
font-size: 1rem;
}
/* The pinned apps under Apps have no room in the rail. */
.sidebar.collapsed .nav-item--sub {
display: none;
}
.sidebar.collapsed .sidebar-footer {
justify-content: center;
flex-direction: column;
gap: var(--spacing-xs);
/* The controls stack, but the row's inline padding still left them wider
than the collapsed rail, so their edges were clipped. */
padding-inline: 2px;
}
.sidebar.collapsed .theme-toggle {
padding: 0;
}
.sidebar.collapsed .theme-toggle .nav-label {
display: none;
}
/* Theme toggle */
.theme-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: var(--control-h);
min-height: var(--control-h);
background: none;
border: 1px solid transparent;
color: var(--color-text-secondary);
padding: 0 var(--spacing-sm);
border-radius: var(--radius-md);
cursor: pointer;
font-size: 1rem;
transition: color var(--dk-dur-1) var(--dk-ease-out),
border-color var(--dk-dur-1) var(--dk-ease-out),
background var(--dk-dur-1) var(--dk-ease-out),
transform var(--dk-dur-1) var(--dk-ease-out);
}
.theme-toggle:hover {
color: var(--color-text-primary);
background: var(--color-bg-hover);
}
.theme-toggle:active { transform: scale(var(--dk-press-scale)); }
.theme-toggle:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.theme-toggle__icon {
display: inline-block;
animation: themeIconIn var(--duration-normal) var(--ease-default);
}
@keyframes themeIconIn {
from { opacity: 0; transform: rotate(-90deg) scale(0.7); }
to { opacity: 1; transform: rotate(0) scale(1); }
}
/* Language switcher */
.language-switcher {
position: relative;
display: inline-flex;
}
.language-switcher-trigger {
display: inline-flex;
align-items: center;
gap: 6px;
/* The globe is 16px wide: 12px of padding centres it on the icon column. */
padding-left: 12px;
}
.language-switcher-trigger .lai-icon { width: 16px; height: 16px; }
.language-switcher-code {
font-size: var(--text-xs);
font-weight: var(--font-weight-bold);
letter-spacing: 0.04em;
}
.sidebar.collapsed .language-switcher-code {
display: none;
}
.language-switcher-menu {
position: absolute;
bottom: calc(100% + 8px);
left: 0;
min-width: 220px;
/* The menu opens upwards from the sidebar footer, inside a scrollable
sidebar. Cap its height to the viewport so it stays within the sidebar
box instead of being clipped at the top on short windows. */
max-height: calc(100dvh - 64px);
overflow-y: auto;
overscroll-behavior: contain;
background: var(--color-surface-float);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
list-style: none;
margin: 0;
padding: 4px;
z-index: 1000;
transform-origin: bottom left;
animation: dk-pop-in var(--dk-dur-2) var(--dk-ease-spring) both;
}
.sidebar.collapsed .language-switcher-menu {
left: calc(100% + 8px);
bottom: 0;
transform-origin: bottom left;
}
.language-switcher-menu li {
margin: 0;
}
.language-switcher-option {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
background: transparent;
border: 0;
padding: var(--spacing-xs) var(--spacing-sm);
min-height: 40px;
color: var(--color-text-secondary);
cursor: pointer;
border-radius: var(--radius-md);
font-size: var(--text-base);
text-align: left;
}
.language-switcher-option:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.language-switcher-option:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
}
.language-switcher-option.active {
color: var(--color-primary);
}
.language-switcher-flag {
font-weight: 600;
font-size: 0.7rem;
width: 22px;
letter-spacing: 0.04em;
}
.language-switcher-name {
flex: 1;
}
.language-switcher-check {
font-size: 0.75rem;
}
/* App boot fallback (rendered while initial i18n namespaces load) */
.app-boot-spinner {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-bg-primary);
}
.app-boot-spinner-dot {
width: 36px;
height: 36px;
border-radius: 50%;
border: 3px solid var(--color-border-subtle);
border-top-color: var(--color-primary);
animation: app-boot-spin 0.8s linear infinite;
}
@keyframes app-boot-spin {
to { transform: rotate(360deg); }
}
/* Operations strip: always exactly one line. Anything that does not fit here
belongs on /app/activity. */
.operations-strip {
display: flex;
align-items: center;
gap: var(--spacing-sm);
min-height: 40px;
padding: var(--spacing-xs) var(--spacing-md);
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.8125rem;
/* An install error is arbitrarily long text. This clips it; the shrinking
is done by min-width on .main-content above and on __detail below. */
overflow: hidden;
}
.operations-strip--error { background: var(--color-error-light); }
.operations-strip__icon { flex: none; font-size: 0.8125rem; }
.operations-strip--error .operations-strip__icon { color: var(--color-error); }
.operations-strip--queued .operations-strip__icon { color: var(--color-text-muted); }
.operations-strip--staging .operations-strip__icon { color: var(--color-info); }
.operations-strip--removing .operations-strip__icon { color: var(--color-warning); }
.operations-strip--done .operations-strip__icon { color: var(--color-success); }
.operations-strip__spinner {
width: 13px;
height: 13px;
flex: none;
border-radius: 50%;
border: 2px solid var(--color-primary-light);
border-top-color: var(--color-primary);
animation: operationsStripSpin 0.9s linear infinite;
}
@keyframes operationsStripSpin { to { transform: rotate(360deg); } }
.operations-strip__verb { flex: none; color: var(--color-text-secondary); white-space: nowrap; }
.operations-strip__name {
font-family: var(--font-mono);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* overflow: hidden zeroes the automatic minimum size, so without a floor the
name loses the shrink contest to a long error and the identity of the
thing that broke is the first casualty. */
min-width: 12ch;
}
.operations-strip__sep { flex: none; color: var(--color-border-strong); }
/* min-width and overflow are what let these shrink: a nowrap flex item's
automatic minimum size is otherwise its full content width. */
.operations-strip__detail,
.operations-strip__bytes {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
color: var(--color-text-muted);
font-size: 0.75rem;
white-space: nowrap;
}
.operations-strip__bytes { font-variant-numeric: tabular-nums; }
.operations-strip__spacer { flex: 1 1 auto; min-width: var(--spacing-xs); }
.operations-strip__pct {
flex: none;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size: 0.75rem;
font-weight: 500;
color: var(--color-primary);
}
.operations-strip__track {
flex: 0 0 132px;
height: 3px;
border-radius: var(--radius-full);
background: var(--color-surface-sunken);
overflow: hidden;
}
.operations-strip__fill {
display: block;
height: 100%;
border-radius: var(--radius-full);
background: var(--color-primary);
transition: width var(--duration-slow) var(--ease-spring);
}
.operations-strip--staging .operations-strip__fill { background: var(--color-info); }
.operations-strip__more {
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
min-height: 28px;
padding: 0 var(--spacing-sm);
border-radius: var(--radius-sm);
border: 1px solid var(--color-primary-border);
background: var(--color-primary-light);
color: var(--color-primary);
font-size: 0.6875rem;
font-weight: 600;
white-space: nowrap;
text-decoration: none;
}
.operations-strip__more:hover { background: var(--color-primary); color: var(--color-primary-text); }
.operations-strip__more--neutral {
background: transparent;
border-color: var(--color-border-default);
color: var(--color-text-secondary);
}
.operations-strip__hide {
flex: none;
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
}
.operations-strip__hide:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
/* Narrow screens drop the prose, never the name, the percentage or the
counter. */
@media (max-width: 640px) {
.operations-strip__verb,
.operations-strip__detail,
.operations-strip__bytes,
.operations-strip__sep,
.operations-strip__track { display: none; }
.operations-strip__name { max-width: 45vw; }
}
@media (prefers-reduced-motion: reduce) {
.operations-strip__spinner { animation: none; }
.operations-strip__fill { transition: none; }
}
/* Row-level install indicator, used by the Models and Backends tables. */
.operation-spinner {
width: 16px;
height: 16px;
flex-shrink: 0;
box-sizing: border-box;
border: 2px solid var(--color-border-default);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: inline-block;
}
.operation-bar-container {
flex: 0 1 160px;
min-width: 80px;
height: 3px;
background: var(--color-surface-sunken);
border-radius: var(--radius-full);
overflow: hidden;
}
.operation-bar {
height: 100%;
background: var(--color-primary);
border-radius: var(--radius-full);
transition: width var(--duration-slow) var(--ease-spring);
animation: opsBarBreathe 1.4s ease-in-out infinite;
}
/* Inline install indicator — used in table rows (Models, Backends) */
.inline-install {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.inline-install__row {
display: flex;
align-items: center;
gap: var(--spacing-sm);
min-width: 0;
}
.inline-install__label {
font-size: var(--text-xs);
color: var(--color-primary);
font-weight: var(--font-weight-medium);
white-space: nowrap;
}
/* Toast — a pill on the inverse surface; success and error are solid fills.
The stack sits at the bottom centre and rises in 16px. */
.toast-container {
position: fixed;
left: 50%;
bottom: var(--spacing-md);
transform: translateX(-50%);
width: min(440px, calc(100vw - 32px));
z-index: 1100;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
pointer-events: none;
}
.toast {
pointer-events: auto;
display: flex;
align-items: center;
gap: var(--spacing-sm);
width: 100%;
min-height: 48px;
padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-sm) var(--spacing-md);
background: var(--dk-inverse);
border: 0;
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
color: var(--dk-on-inverse);
font-family: var(--font-sans);
font-size: var(--text-sm);
line-height: 1.4;
animation: dk-toast-in var(--dk-dur-2) var(--dk-ease-out) both;
}
.toast-enter {
opacity: 0;
transform: translateY(16px);
}
.toast-exit {
animation: dk-toast-out var(--dk-dur-1) ease-in both;
}
.toast-success { background: var(--dk-ok); color: var(--dk-on-ok); }
.toast-error { background: var(--dk-error); color: var(--dk-on-error); }
.toast-warning { background: var(--dk-warn); color: var(--dk-on-warn); }
.toast-info { background: var(--dk-inverse); color: var(--dk-on-inverse); }
.toast > span { flex: 1 1 auto; min-width: 0; }
.toast-close {
margin-left: auto;
background: none;
border: none;
border-radius: var(--radius-sm);
color: inherit;
opacity: 0.8;
cursor: pointer;
min-width: 32px;
height: 32px;
display: inline-grid;
place-items: center;
transition: background-color var(--dk-dur-1) ease, opacity var(--dk-dur-1) ease,
transform var(--dk-dur-1) var(--dk-ease-out);
}
.toast-close:hover { opacity: 1; background: color-mix(in srgb, currentColor 16%, transparent); }
.toast-close:active { transform: scale(var(--dk-press-scale)); }
.toast :focus-visible { outline-color: currentColor; }
.toast-link {
font-size: var(--text-sm);
font-weight: var(--font-weight-semibold);
color: inherit;
opacity: 0.9;
text-decoration: underline;
white-space: nowrap;
margin-left: var(--spacing-xs);
}
.toast-link:hover { opacity: 1; }
/* Chat error trace link */
.chat-error-trace-link {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
font-size: 0.8125rem;
color: var(--color-text-secondary);
text-decoration: none;
margin-top: var(--spacing-xs);
}
.chat-error-trace-link:hover {
color: var(--color-primary);
text-decoration: underline;
}
/* Spinner */
.spinner {
display: flex;
align-items: center;
justify-content: center;
}
.spinner-ring {
border: 3px solid var(--color-border-subtle);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.spinner-sm .spinner-ring { width: 16px; height: 16px; }
.spinner-md .spinner-ring { width: 24px; height: 24px; }
.spinner-lg .spinner-ring { width: 40px; height: 40px; }
/* Skeleton shimmer placeholders */
.skeleton {
display: block;
border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--dk-inset) 0%, var(--dk-hover) 50%, var(--dk-inset) 100%);
background-size: 200% 100%;
animation: skeletonShimmer 1.3s linear infinite;
}
.skeleton--line { height: 0.85em; margin: 0.35em 0; width: 100%; }
.skeleton--block { height: 100%; width: 100%; border-radius: var(--radius-md); }
@keyframes skeletonShimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
/* Model selector */
.model-selector {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.875rem;
font-family: inherit;
outline: none;
cursor: pointer;
transition: border-color var(--duration-fast);
min-width: 180px;
}
.model-selector:focus {
border-color: var(--color-border-strong);
}
/* Common page styles — width archetype is opt-in via modifiers.
Default cap fits 9-column data tables on ultrawide displays without
feeling untethered. Add .page--narrow for forms / single-record edit
views, .page--wide for full-bleed (chat shells, log streams). */
.page {
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-2xl);
width: 100%;
max-width: var(--page-max, var(--page-max-default));
margin: 0 auto;
animation: fadeIn var(--duration-normal) var(--ease-default);
}
.page--narrow { --page-max: var(--page-max-narrow); }
.page--medium { --page-max: var(--page-max-medium); }
.page--wide { --page-max: var(--page-max-wide); }
.page-header {
margin-bottom: var(--spacing-xl);
}
/* The editorial title scale from the site: larger, set tighter, and on a line
height near 1 so a two-word title reads as a statement rather than a label.
The typeface is unchanged — DESIGN.md keeps the existing type system — so the
whole difference is scale, tracking and leading, which is where the site gets
its voice from in the first place. */
.page-title {
font-family: var(--font-sans);
font-size: var(--text-3xl);
font-weight: var(--font-weight-bold);
letter-spacing: -0.03em;
line-height: 1.1;
margin-bottom: var(--spacing-xs);
color: var(--color-text-primary);
text-wrap: balance;
}
/* Mid hierarchy tier — between page title and the xs uppercase group labels */
.section-heading {
font-family: var(--font-sans);
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
letter-spacing: -0.01em;
line-height: var(--leading-snug);
color: var(--color-text-primary);
margin: 0;
}
.page-header--editorial {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: var(--spacing-lg);
flex-wrap: wrap;
margin-bottom: var(--space-section, var(--spacing-xl));
}
.page-header__lead { display: flex; flex-direction: column; gap: var(--spacing-xs); min-width: 0; }
.page-header__supporting { color: var(--color-text-secondary); font-size: var(--text-base); margin: 0; max-width: 60ch; }
.page-header__meta { display: flex; align-items: center; gap: var(--spacing-sm); flex-wrap: wrap; }
.page-subtitle {
font-size: var(--text-sm);
color: var(--color-text-secondary);
line-height: var(--leading-normal);
}
/* Cards */
.card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-xl);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
transition: background-color var(--dk-dur-1) var(--dk-ease-out),
border-color var(--dk-dur-1) var(--dk-ease-out),
transform var(--dk-dur-1) var(--dk-ease-out);
}
/* A card is flat at rest and does not move on hover. Only a card that is
itself a link or a button reacts: hover swaps the surface, press scales. */
a.card:hover,
button.card:hover {
background: var(--color-bg-hover);
}
a.card:active,
button.card:active {
transform: scale(var(--dk-press-scale));
}
/* Highlighted card: a raised surface with a firmer edge, no coloured strip. */
.card--accent {
border-color: var(--color-border-strong);
box-shadow: var(--shadow-sm);
}
.card--accent:hover {
box-shadow: var(--shadow-md, var(--shadow-sm));
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--spacing-lg);
}
/* Form rows — consistent label+control rhythm */
.form-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-md);
padding: var(--spacing-md) 0;
border-bottom: 1px solid var(--color-border-divider);
}
.form-row:last-child {
border-bottom: none;
}
.form-row__label {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.form-row__label-text {
font-size: var(--text-base);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.form-row__hint {
font-size: var(--text-sm);
color: var(--color-text-secondary);
line-height: var(--leading-snug);
}
.form-row__control {
flex-shrink: 0;
}
.form-group__title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--text-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-secondary);
padding: var(--spacing-md) 0 var(--spacing-sm);
border-bottom: 1px solid var(--color-border-divider);
margin-bottom: var(--spacing-md);
}
.form-group__title .lai-icon {
color: var(--color-primary);
font-size: var(--text-sm);
}
.form-group__body {
padding-bottom: var(--spacing-md);
}
.form-group__body:last-child {
padding-bottom: var(--spacing-lg);
}
.form-group__actions {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-md) 0 var(--spacing-lg);
border-top: 1px solid var(--color-border-divider);
margin-top: var(--spacing-md);
}
/* Form layout grids */
.form-grid {
display: grid;
gap: var(--spacing-md);
}
.form-grid-2col {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--spacing-md);
}
.form-grid-3col {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--spacing-md);
}
@media (max-width: 720px) {
.form-grid-2col, .form-grid-3col {
grid-template-columns: 1fr;
}
}
.form-field {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.form-field__label {
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.form-field__hint {
font-size: var(--text-xs);
color: var(--color-text-muted);
line-height: var(--leading-snug);
}
/* Button modifiers */
.btn-full {
width: 100%;
}
/* Data table — used by Quantize jobs list, Traces, etc. */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-sm);
}
.data-table th {
text-align: left;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--text-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-secondary);
border-bottom: 1px solid var(--color-border-default);
background: var(--color-surface-sunken);
}
.data-table td {
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--color-border-divider);
vertical-align: middle;
color: var(--color-text-primary);
}
.data-table tbody tr {
transition: background var(--duration-fast);
}
.data-table tbody tr:hover {
background: var(--color-surface-hover);
}
.data-table tbody tr.is-selected {
background: var(--color-primary-light);
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
.data-table__actions {
display: flex;
gap: var(--spacing-xs);
justify-content: flex-end;
}
.data-table__truncate {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Segmented control (Sound mode toggle etc.) */
.segmented {
display: inline-flex;
padding: 3px;
background: var(--color-surface-sunken);
border: 1px solid transparent;
border-radius: var(--radius-md);
margin-bottom: var(--spacing-md);
gap: 2px;
}
.segmented__item {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: 6px var(--spacing-md);
background: transparent;
color: var(--color-text-secondary);
border: none;
border-radius: var(--radius-sm);
font-family: inherit;
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: background var(--dk-dur-1) var(--dk-ease-out), color var(--dk-dur-1) var(--dk-ease-out),
transform var(--dk-dur-1) var(--dk-ease-out);
}
.segmented__item:hover {
color: var(--color-text-primary);
}
.segmented__item:active { transform: scale(var(--dk-press-scale)); }
.segmented__item.is-active {
background: var(--color-surface-raised);
color: var(--color-text-primary);
box-shadow: var(--shadow-subtle);
}
/* Inline checkbox row */
.checkbox-row {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: 10px var(--spacing-md);
font-size: var(--text-sm);
color: var(--color-text-primary);
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
cursor: pointer;
user-select: none;
transition: border-color var(--dk-dur-1) var(--dk-ease-out);
margin-bottom: var(--spacing-md);
}
.checkbox-row:hover {
border-color: var(--color-border-default);
}
.checkbox-row input[type="checkbox"] {
accent-color: var(--color-primary);
cursor: pointer;
}
/* Buttons — flat. Hover swaps the surface, press scales to .97, focus is a
2px ring with a 2px offset. Danger is a tinted wash, never a solid fill. */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
padding: 0 14px;
min-height: var(--control-h);
min-width: var(--control-h);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-family: inherit;
font-weight: var(--font-weight-bold);
letter-spacing: 0;
cursor: pointer;
border: 1px solid transparent;
transition: background-color var(--dk-dur-1) ease,
color var(--dk-dur-1) ease,
border-color var(--dk-dur-1) ease,
transform var(--dk-dur-1) var(--dk-ease-out);
text-decoration: none;
white-space: nowrap;
}
.btn:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
box-shadow: none;
}
/* Global focus ring for any interactive that is not a .btn: a 2px outline in
the accent with a 2px offset. It is wrapped in :where() so component rules
can still refine it. The ring colour clears 3:1 on every surface (see the
contrast check on the theme file). */
:where(a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])):focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.btn-primary {
background: var(--color-primary);
color: var(--color-primary-text);
}
.btn-primary:hover:not(:disabled) {
background: var(--color-primary-hover);
}
.btn-secondary {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
border-color: transparent;
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-bg-hover);
}
.btn-ghost {
background: transparent;
color: var(--color-text-secondary);
border-color: transparent;
}
.btn-ghost:hover:not(:disabled) {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.btn-danger {
background: var(--color-error-light);
color: var(--dk-error-text);
border-color: transparent;
}
.btn-danger:hover:not(:disabled) {
background: color-mix(in srgb, var(--dk-error-wash) 82%, var(--dk-error));
color: var(--dk-error-text);
}
.btn-sm {
padding: 0 var(--spacing-sm);
min-height: var(--control-h-sm);
min-width: var(--control-h-sm);
font-size: var(--text-xs);
letter-spacing: 0;
}
.btn:active:not(:disabled) {
transform: scale(var(--dk-press-scale));
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* Native checkboxes, radios and sliders take the accent. */
input[type="checkbox"],
input[type="radio"],
input[type="range"] {
accent-color: var(--color-primary);
}
/* Toggle switch */
.toggle {
position: relative;
display: inline-block;
width: 38px;
height: 22px;
cursor: pointer;
flex-shrink: 0;
}
.toggle input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.toggle__track {
position: absolute;
inset: 0;
border-radius: var(--radius-full);
background: var(--color-toggle-off);
transition: background-color var(--dk-dur-1) ease;
}
.toggle__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
border-radius: var(--radius-full);
background: var(--color-bg-secondary);
box-shadow: var(--shadow-subtle);
transition: transform var(--dk-dur-2) var(--dk-ease-spring);
}
.toggle--on .toggle__track {
background: var(--color-toggle-on);
}
.toggle--on .toggle__thumb {
transform: translateX(16px);
background: var(--color-primary-text);
}
.toggle:active:not(.toggle--disabled) .toggle__thumb {
transform: scale(var(--dk-press-scale));
}
.toggle--on:active:not(.toggle--disabled) .toggle__thumb {
transform: translateX(16px) scale(var(--dk-press-scale));
}
.toggle:hover:not(.toggle--disabled):not(.toggle--on) .toggle__track {
background: color-mix(in srgb, var(--color-toggle-off) 85%, var(--dk-text));
}
.toggle--on:hover:not(.toggle--disabled) .toggle__track {
background: var(--color-primary-hover);
}
.toggle:has(input:focus-visible) .toggle__track {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.toggle--disabled {
cursor: not-allowed;
opacity: 0.5;
}
/* Inputs — card surface, control-edge border, accent focus ring */
.input {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-md);
padding: var(--spacing-sm) 14px;
min-height: var(--control-h);
font-size: var(--text-base);
font-family: inherit;
letter-spacing: 0;
outline: none;
width: 100%;
transition: border-color var(--dk-dur-1) ease,
background-color var(--dk-dur-1) ease;
}
.input::placeholder {
color: var(--color-text-muted);
opacity: 1;
}
.input:hover:not(:disabled):not(:focus) {
background: var(--color-bg-hover);
}
.input:focus {
border-color: var(--color-primary);
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
box-shadow: none;
}
.input[aria-invalid="true"] {
border-color: var(--color-error);
}
.input:disabled {
background: var(--color-surface-sunken);
color: var(--color-text-muted);
cursor: not-allowed;
opacity: 0.5;
}
select.input {
cursor: pointer;
padding-right: var(--spacing-xl);
}
.input-mono {
font-family: var(--font-mono);
font-size: var(--text-sm);
letter-spacing: -0.01em;
}
.textarea {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-md);
padding: var(--spacing-sm) 14px;
font-size: var(--text-base);
font-family: inherit;
letter-spacing: 0;
outline: none;
width: 100%;
resize: vertical;
min-height: 80px;
line-height: var(--leading-normal);
transition: border-color var(--dk-dur-1) ease,
background-color var(--dk-dur-1) ease;
}
.textarea::placeholder {
color: var(--color-text-muted);
opacity: 1;
}
.textarea:hover:not(:disabled):not(:focus) {
background: var(--color-bg-hover);
}
.textarea:focus {
border-color: var(--color-primary);
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
box-shadow: none;
}
.textarea[aria-invalid="true"] {
border-color: var(--color-error);
}
.textarea:disabled {
background: var(--color-surface-sunken);
color: var(--color-text-muted);
cursor: not-allowed;
opacity: 0.5;
}
/* CodeMirror editor wrapper */
.code-editor-cm .cm-editor {
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
}
.code-editor-cm .cm-editor.cm-focused {
border-color: var(--color-border-strong);
outline: none;
}
/* Form groups */
.form-group {
margin-bottom: var(--spacing-md);
}
.form-label {
display: block;
font-size: var(--text-sm);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-xs);
font-weight: var(--font-weight-semibold);
}
/* Badges — soft pills, mono caps */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: var(--radius-full);
font-family: var(--font-mono);
font-size: 0.625rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
line-height: 1.4;
font-variant-numeric: tabular-nums;
}
.badge-success {
background: var(--color-success-light);
color: var(--dk-ok-text);
}
.badge-error {
background: var(--color-error-light);
color: var(--dk-error-text);
}
.badge-info {
background: var(--color-info-light);
color: var(--dk-accent-text);
}
.badge-warning {
background: var(--color-warning-light);
color: var(--dk-warn-text);
}
.badge-accent {
background: var(--color-accent-light);
color: var(--dk-accent-text);
}
/* Horizontal row of badges used inside table cells — consistent spacing so
cells line up regardless of how many badges are present. */
.badge-row {
display: inline-flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
/* Vertically stacked cell content (e.g. version + update chip + drift chip).
Keeps rows readable at scale without inline style={{...}} everywhere. */
.cell-stack {
display: flex;
flex-direction: column;
gap: 4px;
align-items: flex-start;
}
.cell-mono {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-text-primary);
}
.cell-muted {
color: var(--color-text-muted);
font-size: var(--text-xs);
}
.cell-subtle {
color: var(--color-text-muted);
font-size: var(--text-xs);
font-weight: 400;
margin-left: 8px;
}
.cell-name {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
font-weight: 500;
}
.cell-name > .lai-icon {
color: var(--color-accent);
font-size: var(--text-xs);
}
.row-actions {
display: flex;
gap: var(--spacing-xs);
justify-content: flex-end;
align-items: center;
}
/* Softer delete button for dense tables — the destructive confirm dialog
already owns the "are you sure" affordance, so the button itself doesn't
need to scream. Keeps the delete red readable without dominating rows. */
.btn.btn-danger-ghost {
background: transparent;
color: var(--dk-error-text);
border-color: transparent;
}
.btn.btn-danger-ghost:hover:not(:disabled) {
background: var(--color-error-light);
color: var(--dk-error-text);
border-color: transparent;
}
/* Small count pill used inside tabs ("(3) ↑ 2") so update counts are
glanceable without extra rows of UI. */
.tab-pill {
display: inline-flex;
align-items: center;
gap: 3px;
margin-left: 6px;
padding: 1px 6px;
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 600;
line-height: 1.4;
}
.tab-pill--warning {
background: var(--color-warning-light);
color: var(--dk-warn-text);
}
/* Stat cards — uniform-height cluster metrics for the Nodes dashboard.
The metric's semantic colour (success/warning/error/primary) is carried by
the icon and the value, with no strip on the edge. The icon sits on the right and the value is left-aligned and
prominent so you can scan a row of cards without reading labels. */
/* Renamed off `.stat-grid` in favour of the children it actually holds. The
split views introduced a second, unrelated `.stat-grid` for detail panes
further down this file, and being later it won every shared property: these
cards were being laid out on that component's 120px columns and 1px hairline
gap, which crushed their labels at every width. */
.stat-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.stat-card {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-sm);
padding: var(--spacing-md);
min-height: 96px;
background: var(--color-bg-raised, var(--color-bg-secondary));
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
transition: transform var(--duration-fast) var(--ease-default),
box-shadow var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default);
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
border-color: var(--color-border-strong);
}
.stat-card__body {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.stat-card__label {
font-size: var(--text-xs);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-text-muted);
white-space: normal;
line-height: 1.2;
}
.stat-card__value {
font-size: var(--text-2xl);
font-weight: 600;
font-family: var(--font-mono);
line-height: 1;
color: var(--color-text-primary);
word-break: break-word;
}
.stat-card__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--stat-accent, var(--color-text-muted)) 12%, transparent);
color: var(--stat-accent, var(--color-text-muted));
font-size: var(--text-lg);
flex-shrink: 0;
}
/* Subtle "Register a new worker" trigger replacing the broken-text chevron
link. Still opens the same hint card — just reads like a button now. */
.nodes-add-worker {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
background: transparent;
border: 1px dashed var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-secondary);
font-size: var(--text-sm);
font-family: inherit;
font-weight: 500;
cursor: pointer;
margin-bottom: var(--spacing-md);
transition: background var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default);
}
.nodes-add-worker:hover {
background: var(--color-bg-raised, var(--color-bg-secondary));
border-color: var(--color-border-strong);
color: var(--color-text-primary);
}
/* Shared FilterBar layout — search strip + chip row + toggle strip. Lives
outside the .filter-bar chip row so the padding and wrapping behavior is
consistent between the Backends gallery and the System tabs. */
.filter-bar-group {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.filter-bar-group__search {
min-width: 200px;
flex: 1;
}
.filter-bar-group__row {
display: flex;
gap: var(--spacing-md);
align-items: center;
flex-wrap: wrap;
}
.filter-bar-group__right {
display: flex;
gap: var(--spacing-md);
align-items: center;
flex-wrap: wrap;
padding-left: var(--spacing-md);
border-left: 1px solid var(--color-border-subtle);
margin-left: auto;
}
.filter-bar-group__toggle {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--text-sm);
color: var(--color-text-secondary);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.filter-btn__count {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 6px;
min-width: 18px;
padding: 0 5px;
background: color-mix(in srgb, currentColor 18%, transparent);
border-radius: var(--radius-full);
font-size: 0.625rem;
font-weight: 600;
}
/* Popover — floating surface anchored to a trigger element. Uses the .card
base so theming is free, adds z-index + fixed-position + scroll cap so it
behaves on tables with many rows. Kept deliberately unstyled beyond that
— content is expected to provide its own header/body structure. */
.popover {
position: fixed;
z-index: 200;
min-width: 260px;
max-width: min(420px, 95vw);
max-height: min(420px, 70vh);
display: flex;
flex-direction: column;
padding: 0; /* sections provide their own padding */
overflow: hidden;
border-radius: var(--radius-xl);
background: var(--color-surface-float);
border-color: var(--color-border-card);
box-shadow: var(--shadow-lg);
/* Scales from .96 to 1 out of the trigger corner. */
transform-origin: top left;
animation: dk-pop-in var(--dk-dur-2) var(--dk-ease-spring) both;
}
.popover--up { transform-origin: bottom left; }
.popover__header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--color-border-subtle);
font-size: var(--text-sm);
}
.popover__scroll {
overflow: auto;
padding: 0;
}
.popover__table {
margin: 0;
width: 100%;
}
.popover__table th {
position: sticky;
top: 0;
background: var(--color-bg-raised, var(--color-bg-secondary));
z-index: 1;
}
/* Inline-table chip trigger — looks like a badge but is a button (cursor,
focus ring inherited from global :focus-visible). */
.chip-trigger {
border: none;
cursor: pointer;
font-family: inherit;
}
.chip-trigger:hover {
filter: brightness(1.08);
}
/* Truncate + ellipsize a long cell (e.g. OCI digest) without breaking the
table layout. Tooltip preserves the full value. */
.cell-truncate {
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Compact empty-state used inside expanded drawer sections (e.g. "No
models loaded on this node"). Dimmer than the page-level .empty-state
because it lives inside another container and shouldn't compete with
the row's primary content. */
.drawer-empty {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-bg-tertiary);
border: 1px dashed var(--color-border-subtle);
border-radius: var(--radius-md);
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.drawer-empty > .lai-icon {
font-size: var(--text-sm);
color: var(--color-text-muted);
opacity: 0.8;
}
/* Small caps eyebrow inside the drawer's "Manage" disclosure. Replaces the
h4 sub-headings that used to stack inside the drawer — at this depth, an
eyebrow keeps the typographic hierarchy from feeling parallel to the
page-level h1/h2 stack. */
.drawer-eyebrow {
font-size: 0.6875rem;
font-weight: var(--font-weight-semibold);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-text-muted);
margin-bottom: var(--spacing-xs);
}
/* "Manage" disclosure inside the node drawer. The chevron rotates with the
open state so the affordance reads as an accordion, not a link. */
.node-manage > summary {
user-select: none;
outline: none;
}
.node-manage > summary::-webkit-details-marker {
display: none;
}
.node-manage > summary:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
.node-manage__chevron {
font-size: 0.625rem;
transition: transform var(--duration-fast) ease-out;
}
.node-manage[open] > summary .node-manage__chevron {
transform: rotate(90deg);
}
/* Node-status indicator — replaces the tiny bullet with a proper LED-style
dot next to a bold status label. Colors are applied inline from statusConfig
so one primitive handles healthy/unhealthy/draining/pending in one shape. */
.node-status {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: var(--text-sm);
font-weight: 600;
}
/* Row-chevron cell — small 20px toggle used in table rows that expand.
The row itself is still clickable; the chevron provides the visible
affordance users were missing. */
.row-chevron {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: var(--text-xs);
color: var(--color-text-muted);
transition: transform var(--duration-fast) var(--ease-default);
}
.row-chevron.is-expanded {
transform: rotate(90deg);
color: var(--color-text-primary);
}
/* Upgrade banner — the yellow strip operators see when updates are available.
Mirrors the gallery so both pages speak the same visual language. */
.upgrade-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-md);
padding: var(--spacing-sm) var(--spacing-md);
margin-bottom: var(--spacing-md);
background: var(--color-warning-light);
border: 1px solid var(--color-warning);
border-radius: var(--radius-md);
color: var(--color-warning);
}
.upgrade-banner__text {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
font-weight: 500;
font-size: var(--text-sm);
}
.upgrade-banner__actions {
display: inline-flex;
gap: var(--spacing-xs);
align-items: center;
}
/* Tabs */
/* Segmented control: an inset track, the current tab lifts onto a card. */
.tabs {
display: flex;
width: fit-content;
max-width: 100%;
overflow-x: auto;
gap: var(--spacing-xs);
padding: var(--spacing-xs);
background: var(--color-bg-tertiary);
border-radius: var(--radius-lg);
margin-bottom: var(--spacing-md);
}
.tab {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
min-height: var(--control-h);
padding: 0 var(--spacing-md);
background: none;
border: none;
border-radius: var(--radius-md);
color: var(--color-text-secondary);
font-size: var(--text-base);
font-weight: var(--font-weight-semibold);
font-family: inherit;
white-space: nowrap;
cursor: pointer;
transition: color var(--dk-dur-1) ease, border-color var(--dk-dur-1) ease,
background-color var(--dk-dur-1) ease, transform var(--dk-dur-1) var(--dk-ease-out);
}
.tab:hover {
color: var(--color-text-primary);
background: var(--color-bg-hover);
}
.tab:active { transform: scale(var(--dk-press-scale)); }
.tab-active,
.tab-active:hover {
color: var(--color-text-primary);
background: var(--color-bg-secondary);
box-shadow: var(--shadow-sm);
}
/* Tables */
.table-container {
overflow-x: auto;
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
/* Prevent flexbox container from stretching due to large inner HTML tables */
width: 1px;
min-width: 100%;
}
/* ResponsiveTable: stack dense tables into label/value cards on narrow screens
instead of a sideways scroll. Labels come from data-label (mirrored from the
<thead> by the ResponsiveTable component). */
@media (max-width: 640px) {
/* Direct-child selectors only: a nested table inside a cell renders normally.
min-width override defeats any inline min-width set for the desktop layout. */
.table--responsive { border: none; min-width: 0 !important; }
.table--responsive > thead { display: none; }
.table--responsive > tbody > tr {
display: block;
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
background: var(--color-surface-raised);
margin: var(--spacing-sm);
padding: var(--spacing-xs) var(--spacing-sm);
}
.table--responsive > tbody > tr > td {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-md);
border: none;
padding: var(--spacing-xs) 0;
text-align: right;
}
.table--responsive > tbody > tr > td[data-label]::before {
content: attr(data-label);
font-weight: var(--font-weight-semibold);
color: var(--color-text-muted);
text-align: left;
margin-right: auto;
}
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
/* Scheduling */
/* Key-value chip builder (node selectors, node labels) */
.kvchips__chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: var(--spacing-xs);
}
.kvchips__chip {
align-items: center;
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
display: inline-flex;
font-family: var(--font-mono);
font-size: 0.75rem;
gap: 4px;
padding: 2px 8px;
}
.kvchips__chip-remove {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.625rem;
padding: 0;
}
.kvchips__row {
align-items: stretch;
display: flex;
gap: var(--spacing-xs);
position: relative;
}
.kvchips__add {
min-height: 36px;
}
/* Anchored to the input row so the list covers what follows the field rather
than pushing the rest of the form down as the user types. */
.kvchips__suggestions {
background: var(--color-surface-float);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
left: 0;
list-style: none;
margin: 4px 0 0;
max-height: 220px;
overflow-y: auto;
padding: 4px;
position: absolute;
right: 0;
top: 100%;
z-index: 20;
}
.kvchips__suggestion {
background: none;
border: none;
border-radius: var(--radius-sm);
color: var(--color-text-primary);
cursor: pointer;
display: block;
font-family: var(--font-mono);
font-size: 0.8125rem;
padding: var(--spacing-xs) var(--spacing-sm);
text-align: left;
width: 100%;
}
.kvchips__suggestion:hover,
.kvchips__suggestion--active {
background: var(--color-bg-tertiary);
}
.table th {
text-align: left;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
font-weight: 500;
font-size: 0.8125rem;
border-bottom: 1px solid var(--color-border-subtle);
}
.table td {
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--color-border-divider);
color: var(--color-text-primary);
transition: background var(--duration-fast) var(--ease-default);
}
.table tr:last-child td {
border-bottom: none;
}
.table tr:hover td {
background: var(--color-surface-hover);
}
/* Collapsible */
.collapsible-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) 0;
cursor: pointer;
color: var(--color-text-secondary);
font-size: 0.8125rem;
font-weight: 500;
user-select: none;
}
button.collapsible-header {
width: 100%;
border: 0;
background: transparent;
font-family: inherit;
text-align: left;
}
button.collapsible-header:hover {
color: var(--color-text-primary);
}
button.collapsible-header:focus-visible {
border-radius: var(--radius-sm);
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.collapsible-header .lai-icon {
transition: transform var(--duration-fast);
}
.collapsible-header.open .lai-icon {
transform: rotate(90deg);
}
/* Search bar */
.search-bar {
position: relative;
}
.search-bar .search-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-muted);
font-size: 0.875rem;
}
.search-bar .input {
padding-left: 40px;
}
/* Pagination */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
margin-top: var(--spacing-lg);
}
.pagination-btn {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
cursor: pointer;
font-size: 0.8125rem;
transition: all var(--duration-fast);
}
.pagination-btn:hover:not(:disabled) {
border-color: var(--color-primary-border);
color: var(--color-primary);
}
.pagination-btn.active {
background: var(--color-primary);
color: var(--color-primary-text);
border-color: var(--color-primary);
}
.pagination-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Filter buttons */
.filter-bar {
display: flex;
gap: var(--spacing-xs);
flex-wrap: wrap;
margin-bottom: var(--spacing-md);
}
.filter-btn {
min-height: var(--control-h-sm);
padding: 0 var(--spacing-md);
background: var(--color-surface-raised);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-full);
cursor: pointer;
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
font-family: inherit;
transition: background-color var(--dk-dur-1) ease, color var(--dk-dur-1) ease,
border-color var(--dk-dur-1) ease, transform var(--dk-dur-1) var(--dk-ease-out);
}
.filter-btn:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.filter-btn:active:not(:disabled) { transform: scale(var(--dk-press-scale)); }
.filter-btn.active {
background: var(--color-primary-light);
color: var(--dk-accent-text);
border-color: var(--color-primary-border);
}
/* The global :focus-visible ring is wrapped in :where(), so it carries the
specificity of a bare :focus-visible - the same (0,1,0) as .filter-btn, which
is declared later and therefore won with its resting shadow. Chips were left
with no visible keyboard focus indicator at all; this restates the ring where
it outranks both the resting and the hover shadow. */
.filter-btn:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
/* A chip is disabled when the selected backend cannot serve that use case. It
keeps its resting colours so the row still reads as a set, dimmed enough to
register as out of play. */
.filter-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
box-shadow: none;
}
.filter-btn:disabled:hover {
transform: none;
box-shadow: none;
background: var(--color-surface-raised);
color: var(--color-text-secondary);
border-color: var(--color-border-default);
}
@media (prefers-reduced-motion: reduce) {
.filter-btn {
transition: none;
}
.filter-btn:hover {
transform: none;
}
}
/* Login page */
.login-page {
min-height: 100vh;
min-height: 100dvh;
background: var(--color-bg-primary);
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
}
.login-card {
width: 100%;
max-width: 400px;
padding: var(--spacing-xl);
}
.login-header {
text-align: center;
margin-bottom: var(--spacing-xl);
}
.login-logo {
width: 56px;
height: 56px;
margin-bottom: var(--spacing-md);
}
.login-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: var(--spacing-xs);
color: var(--color-text-primary);
}
.login-tagline {
color: var(--color-text-secondary);
font-size: 0.9375rem;
margin-bottom: var(--spacing-sm);
}
.login-subtitle {
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.login-alert {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
font-size: 0.8125rem;
margin-bottom: var(--spacing-md);
}
.login-alert-error {
background: var(--color-error-light);
color: var(--color-error);
border: 1px solid var(--color-error-border);
}
.login-alert-success {
background: var(--color-success-light);
color: var(--color-success);
border: 1px solid var(--color-success-border);
}
.login-divider {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin: var(--spacing-lg) 0;
color: var(--color-text-muted);
font-size: 0.8125rem;
}
.login-divider::before,
.login-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--color-border-subtle);
}
.login-footer {
text-align: center;
margin-top: var(--spacing-md);
font-size: 0.8125rem;
color: var(--color-text-secondary);
}
.login-link {
background: none;
border: none;
color: var(--color-primary);
cursor: pointer;
padding: 0;
font: inherit;
}
.login-link:hover {
color: var(--color-primary-hover);
}
.login-token-toggle {
margin-top: var(--spacing-lg);
text-align: center;
}
.login-token-toggle > button {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.75rem;
padding: 0;
font: inherit;
font-size: 0.75rem;
}
.login-token-toggle > button:hover {
color: var(--color-text-secondary);
}
.login-token-form {
margin-top: var(--spacing-sm);
}
/* Empty state */
/* Empty state — editorial: eyebrow rule + large mono headline + lede.
Existing pages pass `icon`, `title`, `text` children into .empty-state as
nested elements; the rules below re-style each without JSX changes. */
.empty-state {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-md);
text-align: left;
padding: var(--spacing-3xl) var(--spacing-xl);
max-width: 640px;
margin: 0 auto;
animation: fadeIn var(--duration-slow) var(--ease-spring);
}
/* Center the column within its parent without shrinking type. */
.empty-state > * { max-width: 100%; }
.empty-state-icon {
font-size: 1.5rem;
color: var(--color-primary);
opacity: 0.7;
margin: 0;
}
.empty-state-title {
font-family: var(--font-sans);
font-size: clamp(1.5rem, 3vw, var(--text-3xl));
font-weight: var(--font-weight-semibold);
letter-spacing: -0.02em;
color: var(--color-text-primary);
margin: 0;
line-height: 1.15;
}
.empty-state-text {
color: var(--color-text-secondary);
font-size: var(--text-base);
line-height: var(--leading-normal);
max-width: 52ch;
margin: 0;
}
/* Secondary to .empty-state-text: a hint about a default that may be narrowing
the result, so it must not compete with the reason stated above it. */
.empty-state-hint {
color: var(--color-text-muted);
font-size: var(--text-sm);
line-height: var(--leading-normal);
max-width: 52ch;
margin: 0;
}
/* Opt-in editorial sub-parts — pages can adopt these class names over time */
.empty-state__eyebrow {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
font-family: var(--font-mono);
font-size: 0.625rem;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--color-text-muted);
}
.empty-state__eyebrow::before {
content: "";
display: block;
width: 24px;
height: 1px;
background: var(--color-border-strong);
}
.empty-state__actions {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
margin-top: var(--spacing-xs);
}
/* Animations */
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(12px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes toastSlideIn {
from { opacity: 0; transform: translateX(12px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes popIn {
from { opacity: 0; transform: scale(0.96) translateY(4px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes attentionPulse {
0%, 100% { opacity: 0.65; }
50% { opacity: 1; }
}
@keyframes opsBarBreathe {
0%, 100% { opacity: 0.85; }
50% { opacity: 1; }
}
@keyframes breathingRing {
0%, 100% { box-shadow: inset 0 0 0 1.5px var(--color-primary-light); }
50% { box-shadow: inset 0 0 0 2px var(--color-primary); }
}
@keyframes messageSlideIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes dropdownIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Page route transitions */
.page-transition {
/* Fill backwards, not both: a finished animation must not leave a transform
on the page, because that would make fixed overlays (dialog veils) cover
only the main column instead of the viewport. */
animation: pageReveal 250ms ease-out backwards;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
min-width: 0;
}
@keyframes pageReveal {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* Orchestrated child stagger - opt-in via .reveal-stagger on a container.
Each direct child reveals with an incremental delay driven by --reveal-index. */
.reveal-stagger > * {
animation: pageReveal var(--duration-reveal) var(--ease-reveal) both;
animation-delay: calc(var(--reveal-index, 0) * 60ms);
}
/* Chat-specific styles */
.chat-layout {
display: flex;
flex: 1;
min-height: 0;
min-width: 0;
overflow: hidden;
position: relative;
}
.chat-sidebar {
width: 260px;
background: var(--color-bg-secondary);
border-right: 1px solid var(--color-border-subtle);
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
transition: width 200ms ease, opacity 150ms ease;
}
.chat-sidebar.hidden {
width: 0;
border-right: none;
opacity: 0;
pointer-events: none;
}
.chat-sidebar-header {
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--color-border-subtle);
display: flex;
gap: var(--spacing-xs);
align-items: center;
}
.chat-list {
flex: 1;
overflow-y: auto;
padding: var(--spacing-xs);
scrollbar-width: thin;
scrollbar-color: var(--color-border-subtle) transparent;
}
.chat-list::-webkit-scrollbar { width: 4px; }
.chat-list::-webkit-scrollbar-track { background: transparent; }
.chat-list::-webkit-scrollbar-thumb {
background: var(--color-border-subtle);
border-radius: 2px;
}
.chat-list-item {
display: flex;
align-items: flex-start;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
cursor: pointer;
font-size: var(--text-sm);
color: var(--color-text-secondary);
transition: background var(--duration-fast), color var(--duration-fast);
margin-bottom: 2px;
}
.chat-list-item:hover:not(.active) {
background: var(--color-surface-hover);
color: var(--color-text-primary);
}
.chat-list-item.active {
background: var(--color-primary-light);
color: var(--color-primary);
font-weight: var(--font-weight-medium);
}
.chat-list-item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.chat-list-item-top {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.chat-list-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
font-size: 0.8125rem;
}
.chat-list-item-time {
font-size: 0.625rem;
color: var(--color-text-muted);
white-space: nowrap;
flex-shrink: 0;
}
.chat-list-item-preview {
font-size: 0.6875rem;
color: var(--color-text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
.chat-list-item-delete {
opacity: 0;
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: 2px;
font-size: 0.75rem;
}
.chat-list-item:hover .chat-list-item-delete {
opacity: 1;
}
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
position: relative;
overflow: hidden;
}
.chat-messages {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: var(--spacing-md) var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
scrollbar-width: thin;
scrollbar-color: var(--color-border-subtle) transparent;
transition: padding-top var(--duration-normal) var(--ease-default);
}
.chat-messages::-webkit-scrollbar { width: 6px; }
.chat-messages::-webkit-scrollbar-track { background: transparent; }
/* Floating "jump to latest" pill, shown when scrolled away from the bottom. */
.chat-messages::-webkit-scrollbar-thumb {
background: var(--color-border-subtle);
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: var(--color-border-default);
}
.chat-message {
display: flex;
gap: var(--spacing-sm);
align-self: stretch;
max-width: 100%;
min-width: 0;
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--color-border-divider);
animation: messageSlideIn 250ms ease-out;
}
/* A transcript, not bubbles. Rounded filled asymmetric bubbles fight a system
built on hairlines, and they carry the speaker in shape and side rather than
in words — which is exactly what a mono role label does better, and what
survives being read aloud or printed. Both roles now run full width down one
column, separated by a rule. */
.chat-message-user {
align-self: stretch;
flex-direction: row;
}
.chat-message-assistant {
align-self: flex-start;
}
.chat-message-avatar {
width: 28px;
height: 28px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
flex-shrink: 0;
}
.chat-message-user .chat-message-avatar {
background: var(--color-primary);
color: var(--color-primary-text);
}
/* The left-edge accent plus the role label carry the speaker, so the avatar is
noise for both roles now that neither side has a bubble. */
.chat-message-avatar { display: none; }
.chat-message-bubble {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.chat-message-model {
font-family: var(--font-mono);
font-size: 0.625rem;
font-weight: 500;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.14em;
padding-left: 2px;
}
.chat-message-content {
background: transparent;
border: none;
border-radius: 0;
padding: 0 var(--spacing-md);
font-size: var(--text-base);
line-height: var(--leading-normal);
word-break: break-word;
color: var(--color-text-primary);
}
/* Same treatment as the assistant, in the action tone so the two are still
told apart at a glance without a fill or a corner radius. */
.chat-message-user .chat-message-content {
background: transparent;
color: var(--color-text-primary);
border: none;
border-radius: 0;
padding: 0 var(--spacing-md);
box-shadow: none;
}
.chat-message-content pre {
background: var(--color-bg-primary);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--spacing-sm);
overflow-x: auto;
margin: var(--spacing-sm) 0;
font-size: 0.8125rem;
}
/* Code block with a header (language + copy), injected by enhanceCodeBlocks. */
.code-block {
margin: var(--spacing-sm) 0;
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
background: var(--color-bg-primary);
}
.code-block__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px var(--spacing-sm);
background: var(--color-surface-sunken);
border-bottom: 1px solid var(--color-border-subtle);
}
.code-block__lang {
font-family: var(--font-mono);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-muted);
}
.code-block .code-copy-btn {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: 4px 6px;
border-radius: var(--radius-sm);
font-size: var(--text-sm);
transition: color var(--duration-fast) var(--ease-default),
background var(--duration-fast) var(--ease-default);
}
.code-block .code-copy-btn:hover { color: var(--color-text-primary); background: var(--color-bg-hover); }
.code-block .code-copy-btn--ok { color: var(--color-success); }
/* Inner pre sits flush inside the framed block. */
.code-block pre {
margin: 0 !important;
border: none !important;
border-radius: 0 !important;
}
.chat-message-user .chat-message-content pre {
background: var(--color-surface-sunken);
border-color: var(--color-border-subtle);
color: var(--color-text-primary);
}
.chat-message-user .chat-message-content code {
color: var(--color-text-primary);
}
.chat-message-user .chat-message-content a {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: 2px;
}
.chat-message-content code {
font-family: var(--font-mono);
font-size: 0.8125rem;
}
.chat-message-content p {
margin: var(--spacing-xs) 0;
}
.chat-message-content p:first-child {
margin-top: 0;
}
.chat-message-content p:last-child {
margin-bottom: 0;
}
/* Message action buttons */
.chat-message-actions {
display: flex;
gap: 2px;
opacity: 0;
transition: opacity 150ms;
padding-left: 2px;
}
.chat-message:hover .chat-message-actions {
opacity: 1;
}
.chat-message-actions button {
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border-subtle);
color: var(--color-text-muted);
cursor: pointer;
padding: 3px 6px;
font-size: 0.6875rem;
border-radius: var(--radius-sm);
transition: all 150ms;
}
.chat-message-actions button:hover {
color: var(--color-primary);
border-color: var(--color-primary-border);
background: var(--color-primary-light);
}
.chat-message-edit {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
min-width: min(32rem, 60vw);
}
.chat-message-edit-input {
width: 100%;
min-height: 6rem;
resize: vertical;
border: 1px solid var(--color-primary-border);
border-radius: var(--radius-md);
background: var(--color-bg-primary);
color: var(--color-text-primary);
font: inherit;
line-height: 1.5;
padding: var(--spacing-sm);
}
.chat-message-edit-input:focus {
outline: 2px solid var(--color-primary-light);
outline-offset: 1px;
}
.chat-message-edit-actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-xs);
}
.chat-message-system {
align-self: center;
max-width: 90%;
}
.chat-message-system .chat-message-bubble {
color: var(--color-text-muted);
background: transparent;
border: none;
font-size: 0.7rem;
letter-spacing: 0.01em;
padding: 2px 0;
}
.chat-message-system .chat-message-content {
background: transparent;
border: none;
border-radius: 0;
padding: 2px var(--spacing-sm);
font-size: 0.7rem;
line-height: 1.4;
color: var(--color-text-muted);
}
.chat-message-timestamp {
font-size: 0.6875rem;
color: var(--color-text-muted);
margin-top: 2px;
}
.chat-input-area {
padding: var(--spacing-sm) var(--spacing-md) var(--spacing-md);
background: var(--color-bg-primary);
}
.chat-input-wrapper {
display: flex;
gap: var(--spacing-xs);
align-items: flex-start;
flex-wrap: wrap;
background: var(--color-surface-raised);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--spacing-sm);
transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
}
/* Mode chips (Canvas, MCP) — render at the start of the input wrapper so
the user sees what's armed for the next message. Compact, low-noise
when off; subtly highlighted when on. The MCP trigger inherits the
same look via the nested-selector overrides below. */
/* MCP popover sits above the chip when it's anchored to the input row
(otherwise it would drop off-screen). */
/* When the MCP dropdown lives inside the modes row, make its trigger
match the chip aesthetic without altering the component itself. */
/* Active state when at least one MCP source is selected — UnifiedMCPDropdown
exposes its count via .chat-mcp-badge, so we use :has() to lift the
chip into the "on" look. Falls back to the neutral chip look on browsers
without :has(). */
.chat-input-wrapper:focus-within {
border-color: var(--color-primary);
box-shadow: var(--shadow-sm), 0 0 0 3px var(--color-primary-light);
}
.chat-input {
flex: 1;
background: transparent;
color: var(--color-text-primary);
border: none;
padding: 10px var(--spacing-sm);
font-size: var(--text-base);
font-family: inherit;
outline: none;
resize: none;
min-height: var(--control-h);
max-height: 200px;
line-height: 1.5;
overflow-y: auto;
/* Modern auto-grow — JS auto-grow stays as a fallback for older engines. */
field-sizing: content;
}
.chat-input::placeholder {
color: var(--color-text-muted);
}
.chat-send-btn {
padding: var(--spacing-xs);
background: var(--color-primary);
color: var(--color-primary-text);
border: none;
border-radius: var(--radius-md);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: var(--control-h);
height: var(--control-h);
flex-shrink: 0;
align-self: flex-start;
transition: background var(--duration-fast), transform var(--duration-fast);
}
.chat-send-btn:hover:not(:disabled) {
background: var(--color-primary-hover);
}
.chat-send-btn:disabled {
background: var(--color-bg-tertiary);
color: var(--color-text-muted);
cursor: not-allowed;
}
.chat-send-btn:active:not(:disabled) {
transform: scale(0.92);
}
.chat-streaming-cursor::after {
content: '';
display: inline-block;
width: 6px;
height: 14px;
background: var(--color-primary);
margin-left: 2px;
animation: pulse 1s infinite;
vertical-align: text-bottom;
}
/* Inline streaming speed indicator */
/* Thinking dots animation */
@keyframes thinkingBounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
/* Staging progress indicator (replaces thinking dots during model transfer) */
@media (prefers-reduced-motion: reduce) {
.chat-message { animation: none; }
}
/* Chat empty state */
.chat-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: var(--spacing-lg) var(--spacing-md);
text-align: center;
min-height: 240px;
gap: var(--spacing-md);
}
.chat-empty-state > * { margin-top: 0; margin-bottom: 0; }
.chat-empty-icon {
font-size: 3rem;
color: var(--color-border-default);
margin-bottom: var(--spacing-lg);
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--color-bg-tertiary);
}
.chat-empty-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 var(--spacing-xs);
}
.chat-empty-text {
font-size: 0.875rem;
color: var(--color-text-secondary);
margin: 0 0 var(--spacing-lg);
max-width: 400px;
}
.chat-empty-hints {
display: flex;
gap: var(--spacing-md);
font-size: 0.75rem;
color: var(--color-text-muted);
}
.chat-empty-hints span {
display: flex;
align-items: center;
gap: 4px;
}
.chat-empty-hints .lai-icon {
font-size: 0.625rem;
}
/* Recent strip on the empty state — replaces the old persistent
conversation sidebar. Visible only while messages.length === 0. */
/* ChatsMenu — popover replacing the old persistent history sidebar. */
.chats-menu {
position: relative;
display: inline-block;
flex-shrink: 0;
}
.chats-menu-popover {
position: absolute;
top: calc(100% + 4px);
left: 0;
z-index: 100;
width: 320px;
max-width: calc(100vw - var(--spacing-md) * 2);
background: var(--color-surface-float);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
max-height: min(70vh, 520px);
animation: dropdownIn 120ms ease-out;
}
.chats-menu-list {
flex: 1;
overflow-y: auto;
padding: var(--spacing-xs);
scrollbar-width: thin;
}
/* Mobile: popover becomes a full-width drawer anchored under the header. */
@media (max-width: 639px) {
.chats-menu-popover {
position: fixed;
top: 56px;
left: 0;
right: 0;
width: 100%;
max-width: 100%;
border-radius: 0;
border-left: none;
border-right: none;
max-height: calc(100dvh - 56px);
}
}
/* Activity group (thinking + tools collapsed into one line) */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.chat-activity-group {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.chat-activity-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-sm);
padding: 6px 12px;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
color: var(--color-text-muted);
transition: color 150ms;
width: 100%;
text-align: left;
}
.chat-activity-toggle:hover {
color: var(--color-text-secondary);
}
.chat-activity-toggle .lai-icon {
font-size: 0.5rem;
flex-shrink: 0;
opacity: 0.4;
}
.chat-activity-summary {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.7rem;
letter-spacing: 0.01em;
}
.chat-activity-count {
display: inline-block;
margin-left: 6px;
padding: 0 5px;
border-radius: 999px;
background: var(--color-bg-tertiary);
font-size: 0.6rem;
color: var(--color-text-muted);
}
.chat-activity-shimmer {
background: linear-gradient(
90deg,
var(--color-text-muted) 0%,
var(--color-text-muted) 40%,
var(--color-primary) 50%,
var(--color-text-muted) 60%,
var(--color-text-muted) 100%
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: shimmer 3s ease-in-out infinite;
}
.chat-activity-details {
display: flex;
flex-direction: column;
padding: 2px 0 6px;
min-width: 0;
overflow: hidden;
}
.chat-activity-item {
padding: 3px 12px;
font-size: 0.7rem;
color: var(--color-text-muted);
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
overflow: hidden;
}
.chat-activity-item-label {
font-size: 0.575rem;
font-weight: 600;
color: var(--color-text-muted);
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.chat-activity-item-content {
font-size: 0.8rem;
color: var(--color-text-secondary);
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
line-height: 1.5;
word-break: break-word;
overflow-wrap: anywhere;
min-width: 0;
}
.chat-activity-item-content.chat-activity-live {
max-height: 300px;
}
.chat-activity-item-content p { margin: 0 0 4px; }
.chat-activity-item-content p:last-child { margin-bottom: 0; }
.chat-activity-item-content pre {
background: var(--color-bg-tertiary);
padding: var(--spacing-xs);
border-radius: var(--radius-sm);
overflow-x: auto;
font-size: 0.75rem;
white-space: pre-wrap;
word-break: break-word;
}
.chat-activity-item-content code {
word-break: break-word;
overflow-wrap: anywhere;
}
/* Context window progress bar */
/* Chat header */
.chat-header {
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--color-border-card);
display: flex;
align-items: center;
gap: var(--spacing-sm);
background: var(--color-bg-secondary);
flex-shrink: 0;
min-height: 56px;
}
.chat-header-title {
font-size: var(--text-base);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 0 1 auto;
min-width: 0;
}
.chat-header-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: var(--spacing-xs);
flex-shrink: 0;
}
.chat-header-actions .btn-secondary.active {
background: var(--color-primary-light);
border-color: var(--color-primary-border);
color: var(--color-primary);
}
/* Chat MCP dropdown */
.chat-mcp-dropdown {
position: relative;
display: inline-block;
}
.chat-mcp-dropdown .btn {
display: flex;
align-items: center;
gap: 5px;
}
.chat-mcp-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 9px;
background: rgba(255,255,255,0.25);
font-size: 0.7rem;
font-weight: 600;
line-height: 1;
}
.chat-mcp-dropdown-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
z-index: 100;
min-width: 240px;
max-height: 320px;
overflow-y: auto;
background: var(--color-surface-float);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
animation: dropdownIn 120ms ease-out;
}
.chat-mcp-dropdown-loading,
.chat-mcp-dropdown-empty,
.chat-mcp-dropdown-error {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.8125rem;
color: var(--color-text-secondary);
}
.chat-mcp-dropdown-error {
color: var(--color-error);
}
.chat-mcp-dropdown-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-xs) var(--spacing-md);
border-bottom: 1px solid var(--color-border-divider);
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.chat-mcp-select-all {
background: none;
border: none;
padding: 0;
font-size: 0.75rem;
color: var(--color-accent);
cursor: pointer;
text-transform: none;
letter-spacing: 0;
}
.chat-mcp-select-all:hover {
text-decoration: underline;
}
.chat-mcp-server-item {
display: flex;
align-items: center;
gap: 8px;
padding: var(--spacing-xs) var(--spacing-md);
cursor: pointer;
transition: background 120ms;
}
.chat-mcp-server-item:hover {
background: var(--color-bg-hover);
}
.chat-mcp-server-item--error {
cursor: not-allowed;
}
.chat-mcp-server-item input[type="checkbox"] {
flex-shrink: 0;
}
.chat-mcp-server-info {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.chat-mcp-server-name {
font-size: 0.8125rem;
font-weight: 500;
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-mcp-server-name-row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.chat-mcp-server-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.chat-mcp-server-status--connected {
background: var(--color-success);
}
.chat-mcp-server-status--error {
background: var(--color-error);
}
.chat-mcp-server-tools {
font-size: 0.7rem;
color: var(--color-text-tertiary);
}
.chat-mcp-server-error {
font-size: 0.7rem;
color: var(--color-error);
overflow-wrap: anywhere;
}
/* Client MCP status indicators */
.chat-client-mcp-status {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
background: var(--color-text-tertiary);
}
.chat-client-mcp-status-connected {
background: var(--color-success);
}
.chat-client-mcp-status-connecting {
background: var(--color-warning);
animation: pulse 1s infinite;
}
.chat-client-mcp-status-error {
background: var(--color-error);
}
.chat-client-mcp-status-disconnected {
background: var(--color-text-tertiary);
}
/* Chat model info panel */
/* Settings drawer */
/* Chat search */
.chat-search-wrapper {
position: relative;
margin-bottom: var(--spacing-xs);
}
.chat-search-icon {
position: absolute;
left: 8px;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-muted);
font-size: 0.7rem;
pointer-events: none;
}
.chat-search-input {
width: 100%;
padding: 5px 24px 5px 26px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
font-size: 0.75rem;
color: var(--color-text-primary);
outline: none;
transition: border-color 150ms;
}
.chat-search-input:focus {
border-color: var(--color-primary-border);
}
.chat-search-input::placeholder {
color: var(--color-text-muted);
}
.chat-search-clear {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.7rem;
padding: 2px;
}
/* Chat list item actions */
.chat-list-item-actions {
display: flex;
gap: 2px;
opacity: 0;
transition: opacity 150ms;
flex-shrink: 0;
}
.chat-list-item:hover .chat-list-item-actions {
opacity: 1;
}
.chat-list-item-actions button {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: 2px;
font-size: 0.7rem;
}
.chat-list-item-actions button:hover {
color: var(--color-text-primary);
}
.chat-list-item-actions .chat-list-item-delete:hover {
color: var(--color-error);
}
/* Max tokens/sec badge */
/* Slider styles */
/* Message inline files */
.chat-message-files {
display: flex;
gap: var(--spacing-xs);
flex-wrap: wrap;
margin-top: var(--spacing-xs);
}
/* Image attachments preview as a thumbnail instead of a bare icon. */
.threed-remesh-controls {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
padding: var(--spacing-md);
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
}
.threed-remesh-heading,
.threed-remesh-scale {
display: flex;
justify-content: space-between;
gap: var(--spacing-md);
}
.threed-remesh-heading {
color: var(--color-text-primary);
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
}
.threed-remesh-heading output,
.threed-remesh-scale,
.threed-remesh-ready {
color: var(--color-text-muted);
font-size: var(--text-xs);
}
.threed-remesh-controls input[type="range"] {
width: 100%;
}
.threed-remesh-ready {
margin: var(--spacing-xs) 0 0;
color: var(--color-success);
}
.media-result-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: var(--spacing-sm);
width: 100%;
}
/* Clickable result thumbnail (opens the lightbox). */
.media-result-thumb {
position: relative;
display: block;
padding: 0;
border: none;
background: none;
cursor: zoom-in;
border-radius: var(--radius-md);
overflow: hidden;
}
.media-result-thumb img {
display: block;
width: 100%;
border-radius: var(--radius-md);
}
.media-result-thumb__zoom {
position: absolute;
top: var(--spacing-xs);
right: var(--spacing-xs);
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: var(--radius-sm);
background: var(--color-bg-overlay);
color: var(--color-text-primary);
font-size: var(--text-xs);
opacity: 0;
transition: opacity var(--duration-fast) var(--ease-default);
}
.media-result-thumb:hover .media-result-thumb__zoom,
.media-result-thumb:focus-visible .media-result-thumb__zoom { opacity: 1; }
/* Fullscreen image lightbox. */
.lightbox {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
background: var(--color-modal-backdrop);
animation: fadeIn var(--duration-fast) var(--ease-default);
}
.lightbox__img {
max-width: 92vw;
max-height: 88vh;
object-fit: contain;
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
}
.lightbox__toolbar {
position: absolute;
top: var(--spacing-md);
right: var(--spacing-md);
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
}
.lightbox__count { color: var(--color-text-secondary); font-size: var(--text-sm); margin-right: var(--spacing-xs); }
.lightbox__nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-full);
background: var(--color-surface-raised);
color: var(--color-text-primary);
cursor: pointer;
box-shadow: var(--shadow-md);
}
.lightbox__nav:hover { border-color: var(--color-primary-border); color: var(--color-primary); }
.lightbox__nav--prev { left: var(--spacing-lg); }
.lightbox__nav--next { right: var(--spacing-lg); }
/* ============================================================
Responsive
------------------------------------------------------------
Three viewport tiers, expressed as cascading media queries:
* desktop (≥1024px) — full sidebar, content margin-left = sidebar-width
* tablet (640–1023) — 52px icon rail; tap hamburger to overlay-expand
* mobile (<640px) — sidebar slides off-screen behind a top-bar drawer
Touch-target minimums apply across both tablet and mobile via the
first (max-width: 1023px) block.
============================================================ */
/* Touch-friendly sizing + shared layout simplifications (tablet + mobile) */
@media (max-width: 1023px) {
.hamburger-btn {
min-width: 44px;
min-height: 44px;
padding: 10px 12px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.nav-item {
min-height: 44px;
}
.sidebar-close-btn {
min-width: 44px;
min-height: 44px;
align-items: center;
justify-content: center;
padding: 10px;
}
.mobile-header {
min-height: 56px;
}
/* Layouts that need to collapse to single-column on any narrow viewport */
.chat-sidebar { display: none; }
.page { padding: var(--spacing-md); }
/* The desktop collapse chevron is desktop-only — tablets auto-rail,
mobile uses the hamburger. */
.sidebar-collapse-btn { display: none; }
}
/* Tablet (640–1023): persistent icon rail; tap hamburger to overlay-expand */
@media (max-width: 1023px) and (min-width: 640px) {
.main-content,
.sidebar-is-collapsed .main-content {
margin-left: var(--sidebar-width-collapsed);
}
.mobile-header { display: none; }
.sidebar {
width: var(--sidebar-width-collapsed);
transform: translateX(0);
}
.sidebar.collapsed { width: var(--sidebar-width-collapsed); }
/* Apply collapsed visuals while not pinned-open. These mirror the
existing .sidebar.collapsed desktop rules so we re-use one look. */
.sidebar:not(.open) .nav-label,
.sidebar:not(.open) .nav-external,
.sidebar:not(.open) .sidebar-section-title,
.sidebar:not(.open) .sidebar-section-chevron { display: none; }
.sidebar:not(.open) .sidebar-logo-link { display: none; }
.sidebar:not(.open) .sidebar-logo-icon {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.sidebar:not(.open) .sidebar-header { justify-content: center; }
.sidebar:not(.open) .nav-item {
justify-content: center;
padding: 8px 0;
border-left-width: 2px;
margin-inline: calc((var(--sidebar-width-collapsed) - var(--control-h)) / 2);
}
.sidebar:not(.open) .nav-icon { width: auto; font-size: 1rem; }
/* The pinned apps under Apps have no room in the rail. */
.sidebar:not(.open) .nav-item--sub { display: none; }
.sidebar:not(.open) .sidebar-nav .nav-item.active::after { display: none; }
.sidebar:not(.open) .sidebar-footer {
justify-content: center;
flex-direction: column;
gap: var(--spacing-xs);
padding-inline: 2px;
}
.sidebar:not(.open) .theme-toggle { padding: 0; }
.sidebar:not(.open) .language-switcher-code { display: none; }
.sidebar:not(.open) .sidebar-user-name,
.sidebar:not(.open) .sidebar-logout-btn { display: none; }
/* Center the avatar in the icon rail (mirrors .sidebar.collapsed, which
isn't necessarily present in the tablet :not(.open) state). */
.sidebar:not(.open) .sidebar-user { justify-content: center; }
.sidebar:not(.open) .sidebar-user-link { flex: 0 0 auto; margin: 0; padding: 0; }
/* Pinned open: overlay the full sidebar on top of content */
.sidebar.open {
width: var(--sidebar-width);
box-shadow: var(--shadow-md);
}
.sidebar-close-btn { display: none; }
.sidebar.open .sidebar-close-btn { display: flex; }
.sidebar-overlay {
display: block;
position: fixed;
inset: 0;
background: var(--color-modal-backdrop);
z-index: 40;
animation: dk-veil-in var(--dk-dur-2) ease-out both;
}
}
/* Mobile (<640): sidebar slides off-screen as a drawer */
@media (max-width: 639px) {
.main-content,
.sidebar-is-collapsed .main-content {
margin-left: 0;
}
.mobile-header { display: flex; }
.sidebar {
transform: translateX(-100%);
width: var(--sidebar-width);
/* A closed drawer must leave the tab order; visibility flips after the
slide-out finishes. */
visibility: hidden;
transition: width var(--duration-normal) var(--ease-spring),
transform var(--dk-dur-2) var(--dk-ease-out),
visibility 0s linear var(--dk-dur-2);
}
.sidebar.collapsed { width: var(--sidebar-width); }
.sidebar.open {
transform: translateX(0);
visibility: visible;
box-shadow: var(--shadow-md);
background: var(--color-bg-primary);
transition: width var(--duration-normal) var(--ease-spring),
transform var(--dk-dur-2) var(--dk-ease-out),
visibility 0s;
}
.sidebar-close-btn { display: flex; }
/* When opened on mobile, even if the .collapsed class is present
from desktop preference, force the expanded look — drawer always
shows full labels. */
.sidebar.collapsed .nav-label,
.sidebar.collapsed .nav-external,
.sidebar.collapsed .sidebar-section-title { display: unset; }
.sidebar.collapsed .sidebar-logo-link { display: block; }
.sidebar.collapsed .sidebar-logo-icon { display: none; }
.sidebar.collapsed .sidebar-nav .nav-item.active::after { display: block; }
.sidebar.collapsed .nav-item {
justify-content: flex-start;
padding: 10px var(--spacing-md);
margin-inline: var(--spacing-sm);
border-left-width: 3px;
}
.sidebar.collapsed .nav-icon {
width: 18px;
font-size: 0.85rem;
}
.sidebar.collapsed .sidebar-header { justify-content: space-between; }
.sidebar-overlay {
display: block;
position: fixed;
inset: 0;
background: var(--color-modal-backdrop);
z-index: 40;
animation: dk-veil-in var(--dk-dur-2) ease-out both;
}
}
/* Mobile reflow polish — phone-only (<640) layout adjustments for
page chrome that was designed flex-row on desktop. */
@media (max-width: 639px) {
/* Page header: stack title block + inline-action cluster vertically */
.page-header {
flex-direction: column;
align-items: stretch;
gap: var(--spacing-md);
}
/* Filter chip rows scroll horizontally instead of wrapping into walls */
.filter-bar {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.filter-bar::-webkit-scrollbar { display: none; }
.filter-btn { flex-shrink: 0; }
.search-bar { min-width: 0; }
/* Tables go edge-to-edge; offsetting against .page padding gives full
bleed without changing the table layout itself. */
.table-container {
border-radius: 0;
border-left: 0;
border-right: 0;
margin-inline: calc(-1 * var(--spacing-md));
}
}
/* Reduced motion — disable non-essential transitions for users who
request it. Keeps focus/state changes accessible without animation. */
@media (prefers-reduced-motion: reduce) {
.sidebar,
.page-transition,
.operations-strip,
.page,
.main-content {
transition: none !important;
animation: none !important;
}
}
/* Canvas panel: the wide view that opens beside the thread on demand. It is
shared by Chat and the Agent chat, so it speaks only kit roles. */
.canvas-panel {
position: relative;
width: 52%;
max-width: 820px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--dk-card);
box-shadow: inset 1px 0 0 var(--dk-edge);
overflow: hidden;
animation: canvasSlideIn var(--dk-dur-3) var(--dk-ease-out) both;
}
@keyframes canvasSlideIn {
from { opacity: 0; transform: translateX(24px); }
to { opacity: 1; transform: translateX(0); }
}
/* Drag-to-resize handle on the panel's left edge. */
.canvas-resize-handle {
position: absolute;
top: 0;
left: -3px;
width: 7px;
height: 100%;
cursor: col-resize;
z-index: 2;
background: transparent;
transition: background var(--dk-dur-1) var(--dk-ease-out);
}
.canvas-resize-handle:hover,
.canvas-resize-handle:active { background: var(--dk-accent-wash); }
/* On a narrow window the canvas takes the whole page area; no edge-drag. */
@media (max-width: 1024px) {
.canvas-resize-handle { display: none; }
.canvas-panel {
position: absolute;
inset: 0;
z-index: 40;
width: 100% !important;
max-width: none !important;
}
}
.canvas-header-actions { display: inline-flex; gap: 2px; }
/* Fullscreen: the canvas covers the viewport. */
.canvas-panel--fullscreen {
position: fixed;
inset: 0;
width: 100% !important;
max-width: none !important;
z-index: var(--dk-z-veil);
box-shadow: none;
}
.canvas-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 52px;
padding: 0 10px 0 16px;
box-shadow: inset 0 -1px 0 var(--dk-edge);
flex-shrink: 0;
}
.canvas-panel-title {
font-family: var(--font-mono);
font-weight: 600;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.canvas-icobtn,
.canvas-textbtn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border: 0;
background: none;
color: var(--color-text-secondary);
font: inherit;
cursor: pointer;
transition: background var(--dk-dur-1) var(--dk-ease-out), color var(--dk-dur-1) var(--dk-ease-out);
}
.canvas-icobtn { width: 34px; height: 34px; border-radius: var(--dk-radius-1); font-size: 17px; }
.canvas-textbtn { height: 30px; padding: 0 10px; border-radius: var(--dk-radius-1); font-size: 13px; font-weight: 600; }
.canvas-icobtn:hover,
.canvas-textbtn:hover { background: var(--dk-hover); color: var(--dk-text); }
.canvas-icobtn:focus-visible,
.canvas-textbtn:focus-visible,
.canvas-panel-tab:focus-visible,
.canvas-toggle-btn:focus-visible { outline: 2px solid var(--dk-accent-text); outline-offset: 2px; }
.canvas-panel-tabs {
overflow-x: auto;
display: flex;
gap: 2px;
padding: 6px 8px;
box-shadow: inset 0 -1px 0 var(--dk-edge);
flex-shrink: 0;
scrollbar-width: thin;
}
.canvas-panel-tab {
display: inline-flex;
align-items: center;
gap: 8px;
height: 32px;
padding: 0 12px;
border: 0;
border-radius: var(--dk-radius-1);
background: none;
color: var(--color-text-secondary);
font-family: var(--font-mono);
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
transition: background var(--dk-dur-1) var(--dk-ease-out);
}
.canvas-panel-tab:hover { background: var(--dk-hover); }
.canvas-panel-tab.active {
background: var(--dk-inset);
color: var(--dk-text);
box-shadow: inset 0 0 0 1px var(--dk-edge);
}
.canvas-panel-tab span {
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
}
.canvas-panel-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
box-shadow: inset 0 -1px 0 var(--dk-edge);
flex-shrink: 0;
}
.canvas-toggle-group {
display: inline-flex;
gap: 2px;
padding: 2px;
border-radius: 9px;
background: var(--dk-inset);
box-shadow: inset 0 0 0 1px var(--dk-edge);
}
.canvas-toggle-btn {
height: 28px;
padding: 0 10px;
border: 0;
border-radius: 7px;
background: none;
color: var(--color-text-secondary);
font: inherit;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
}
.canvas-toggle-btn.active {
background: var(--dk-card);
color: var(--dk-text);
box-shadow: inset 0 0 0 1px var(--dk-edge);
}
.canvas-panel-body {
flex: 1;
overflow: auto;
padding: 0;
min-height: 0;
background: var(--dk-inset);
}
.canvas-panel-body pre {
margin: 0;
padding: 16px 20px;
background: none;
color: var(--color-text-secondary);
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.7;
}
/* Artifact card (inline in messages) */
.artifact-card {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
cursor: pointer;
background: var(--color-bg-tertiary);
margin: var(--spacing-sm) 0;
transition: border-color 150ms;
}
.artifact-card:hover {
border-color: var(--color-primary);
}
.artifact-card-icon {
font-size: 1.1rem;
color: var(--color-primary);
flex-shrink: 0;
}
.artifact-card-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.artifact-card-title {
font-weight: 600;
font-size: 0.8125rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.artifact-card-lang {
font-size: 0.7rem;
color: var(--color-text-muted);
}
.artifact-card-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.artifact-card-actions button {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: 4px 6px;
border-radius: var(--radius-sm);
font-size: 0.75rem;
transition: all 150ms;
}
.artifact-card-actions button:hover {
color: var(--color-primary);
background: var(--color-primary-light);
}
/* Resource cards (below agent messages) */
.resource-cards {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
margin-top: var(--spacing-xs);
}
.resource-card {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
background: var(--color-bg-secondary);
transition: border-color 150ms;
}
.resource-card:hover {
border-color: var(--color-primary);
}
.resource-card-thumb {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: var(--radius-sm);
}
.resource-card-label {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.resource-cards-more {
background: none;
border: 1px dashed var(--color-border-default);
border-radius: var(--radius-sm);
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.75rem;
color: var(--color-text-muted);
cursor: pointer;
}
.resource-cards-more:hover {
color: var(--color-primary);
border-color: var(--color-primary);
}
/* Canvas preview types */
.canvas-preview-iframe {
display: block;
width: 100%;
height: 100%;
min-height: 320px;
border: none;
background: white;
}
.canvas-preview-image {
display: block;
max-width: 100%;
margin: 16px auto;
border-radius: var(--dk-radius-2);
}
.canvas-preview-svg {
display: flex;
justify-content: center;
padding: var(--spacing-md);
}
.canvas-preview-svg svg {
max-width: 100%;
height: auto;
}
.canvas-preview-markdown {
padding: 16px 20px;
line-height: 1.65;
}
.canvas-audio-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-lg);
}
.canvas-audio-icon {
font-size: 2rem;
color: var(--color-primary);
}
.canvas-url-card {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
}
.canvas-url-card a {
color: var(--color-primary);
word-break: break-all;
}
/* Canvas mode toggle */
.canvas-mode-toggle {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.canvas-mode-toggle .canvas-mode-label {
font-weight: 500;
cursor: pointer;
}
.canvas-mode-toggle .toggle {
transform: scale(0.8);
}
@media (max-width: 768px) {
.canvas-panel {
position: fixed;
inset: 0;
width: 100%;
z-index: 50;
}
}
@media (max-width: 640px) {
.card-grid {
grid-template-columns: 1fr;
}
.filter-bar {
overflow-x: auto;
flex-wrap: nowrap;
padding-bottom: var(--spacing-xs);
}
.chat-header {
flex-wrap: wrap;
}
.chat-header-title {
max-width: 120px;
}
.chat-empty-hints {
flex-direction: column;
gap: var(--spacing-xs);
}
}
/* MCP App Frame */
.mcp-app-frame-container {
width: 100%;
margin: var(--spacing-sm) 0;
border-radius: var(--border-radius-md);
overflow: hidden;
border: 1px solid var(--color-border-subtle);
}
.mcp-app-iframe {
width: 100%;
border: none;
display: block;
min-height: 100px;
max-height: 600px;
transition: height 0.2s ease;
background: var(--color-bg-primary);
}
.mcp-app-error {
padding: var(--spacing-sm) var(--spacing-md);
color: var(--dk-error-text);
font-size: 0.85rem;
}
.mcp-app-reconnect-overlay {
padding: var(--spacing-sm);
text-align: center;
font-size: 0.8rem;
color: var(--color-text-secondary);
background: var(--color-bg-secondary);
border-top: 1px solid var(--color-border-subtle);
}
/* Confirm Dialog */
.confirm-dialog-backdrop {
position: fixed;
inset: 0;
z-index: 1050;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-modal-backdrop);
animation: dk-veil-in var(--dk-dur-2) ease-out both;
}
.confirm-dialog {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-card);
border-radius: var(--dk-radius-4);
max-width: 420px;
width: 90%;
padding: var(--spacing-lg);
box-shadow: var(--shadow-lg);
animation: dk-dialog-in var(--dk-dur-3) var(--dk-ease-spring) both;
will-change: transform, opacity;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.confirm-dialog-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.confirm-dialog-danger-icon {
color: var(--dk-error-text);
font-size: 1.125rem;
}
.confirm-dialog-title {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.confirm-dialog-body {
font-size: var(--text-base);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-lg);
line-height: 1.5;
}
.confirm-dialog-actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
}
/* The confirm button is the same danger wash as every other danger button. */
/* ─── Home: the console ────────────────────────────────────────────────────
One command bar first, memory folded into a one-line strip, and a list of
conversations worth resuming. Flat surfaces, solid bars, no glow. Colours
are the kit roles (--dk-*). Focus is a 2 px outline with an offset. */
.home-page {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
padding: var(--spacing-xl) var(--spacing-lg) 96px;
}
.home-col {
width: 100%;
max-width: 880px;
margin: 0 auto;
min-width: 0;
}
/* The reveal must not leave a transform behind: it would trap the menus that
hang below the command bar under the sections that follow it. */
.reveal-stagger.home-col > * { animation-fill-mode: backwards; }
.home-col button:focus-visible,
.home-col a:focus-visible,
.home-col summary:focus-visible { outline: 2px solid var(--dk-accent-text); outline-offset: 2px; }
.home-header {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 14px;
padding: 0 0 18px;
}
.home-greeting {
margin: 0;
font-family: var(--font-sans);
font-size: 28px;
line-height: 1.2;
font-weight: 650;
letter-spacing: -0.02em;
}
.home-date {
font-family: var(--font-mono);
font-size: 13px;
font-weight: 500;
color: var(--dk-muted);
}
/* Shared small controls */
.home-dot {
flex: none;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--dk-ok);
}
.home-dot--cold { background: transparent; box-shadow: inset 0 0 0 1.75px var(--dk-control-edge); }
.home-spinner {
flex: none;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid var(--dk-accent-text);
border-right-color: transparent;
animation: dk-spin 900ms linear infinite;
}
.home-ghost {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 32px;
padding: 0 10px;
border: 0;
border-radius: var(--dk-radius-1);
background: none;
color: var(--color-text-secondary);
font: inherit;
font-size: 13px;
font-weight: 550;
text-decoration: none;
cursor: pointer;
transition: background var(--dk-dur-1) var(--dk-ease-out), color var(--dk-dur-1) var(--dk-ease-out), transform var(--dk-dur-1) var(--dk-ease-out);
}
.home-ghost:hover { background: var(--dk-hover); color: var(--dk-text); }
.home-ghost:active { transform: scale(var(--dk-press-scale)); }
.home-ghost code { font-family: var(--font-mono); font-size: 12px; background: none; padding: 0; color: var(--dk-muted); }
.home-primary,
.home-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 40px;
padding: 0 14px;
border: 0;
border-radius: var(--dk-radius-2);
font: inherit;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
transition: background var(--dk-dur-1) var(--dk-ease-out), transform var(--dk-dur-1) var(--dk-ease-out);
}
.home-primary--sm,
.home-secondary--sm { min-height: 32px; padding: 0 12px; border-radius: var(--dk-radius-1); font-size: 13px; }
.home-primary { background: var(--dk-accent); color: var(--dk-on-accent); }
.home-primary:hover:not(:disabled) { background: color-mix(in srgb, var(--dk-accent) 88%, white); }
.home-secondary {
background: var(--dk-card);
color: var(--dk-text);
font-weight: 550;
box-shadow: inset 0 0 0 1px var(--dk-control-edge);
}
.home-secondary:hover:not(:disabled) { background: var(--dk-hover); }
.home-primary:active:not(:disabled),
.home-secondary:active:not(:disabled) { transform: scale(var(--dk-press-scale)); }
.home-primary:disabled,
.home-secondary:disabled { opacity: .6; cursor: default; }
.home-link {
border: 0;
border-radius: 4px;
background: none;
padding: 0;
color: var(--color-text-secondary);
font: inherit;
font-weight: 600;
text-decoration: underline;
text-underline-offset: 3px;
white-space: nowrap;
cursor: pointer;
}
.home-link:hover { color: var(--dk-text); }
/* Command bar */
.home-cmd {
position: relative;
padding: 12px;
border-radius: var(--dk-radius-3);
background: var(--dk-card);
box-shadow: inset 0 0 0 1px var(--dk-control-edge), var(--dk-shadow-rest);
transition: box-shadow var(--dk-dur-2) var(--dk-ease-out);
}
.home-cmd:focus-within { box-shadow: inset 0 0 0 1px var(--dk-accent-text), var(--dk-shadow-rest); }
.home-cmd__top {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 10px;
min-width: 0;
}
.home-pick { position: relative; min-width: 0; }
.home-chip,
.home-cmd__top .chat-mcp-dropdown > .btn {
display: inline-flex;
align-items: center;
gap: 8px;
height: 32px;
min-width: 0;
max-width: 100%;
padding: 0 10px;
border: 0;
border-radius: var(--dk-radius-1);
background: var(--dk-card);
box-shadow: inset 0 0 0 1px var(--dk-control-edge);
color: var(--dk-text);
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
text-transform: none;
letter-spacing: 0;
white-space: nowrap;
cursor: pointer;
transition: background var(--dk-dur-1) var(--dk-ease-out), transform var(--dk-dur-1) var(--dk-ease-out);
}
.home-chip:hover:not(:disabled),
.home-cmd__top .chat-mcp-dropdown > .btn:hover { background: var(--dk-hover); }
.home-chip:active:not(:disabled),
.home-cmd__top .chat-mcp-dropdown > .btn:active { transform: scale(var(--dk-press-scale)); }
.home-chip:disabled { cursor: default; color: var(--dk-muted); }
.home-chip .lai-icon { width: 16px; height: 16px; flex: none; color: var(--color-text-secondary); }
.home-chip__text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The MCP trigger is the app's shared dropdown; this restyles it as a chip. */
.home-cmd__top .chat-mcp-dropdown > .btn { font-family: inherit; font-weight: 550; color: var(--color-text-secondary); }
.home-cmd__top .chat-mcp-dropdown > .btn .chat-mcp-badge {
height: auto;
min-width: 0;
padding: 2px 6px;
border-radius: 99px;
background: var(--dk-accent-wash);
color: var(--dk-accent-text);
font-family: var(--font-mono);
font-size: 11px;
}
.home-slash-hint {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: auto;
color: var(--dk-muted);
font-size: 13px;
white-space: nowrap;
}
/* Menus: the model list and the slash list */
.home-menu,
.dk-cmdlist.home-slash {
background: color-mix(in srgb, var(--dk-card) 94%, var(--dk-text));
border: 1px solid var(--dk-edge);
border-radius: var(--dk-radius-2);
box-shadow: var(--dk-shadow-float);
}
.home-menu {
position: absolute;
z-index: 120;
top: calc(100% + 6px);
left: 0;
min-width: min(400px, calc(100vw - 48px));
max-width: min(560px, calc(100vw - 48px));
padding: 6px;
animation: homeRise var(--dk-dur-2) var(--dk-ease-out) both;
}
.home-menu__filter {
width: 100%;
height: 36px;
margin-bottom: 4px;
padding: 0 10px;
border: 0;
border-radius: var(--dk-radius-1);
background: var(--dk-inset);
box-shadow: inset 0 0 0 1px var(--dk-edge);
color: var(--dk-text);
font: inherit;
font-size: 13px;
}
.home-menu__filter:focus-visible { outline: 2px solid var(--dk-accent-text); outline-offset: 0; }
.home-menu__list { list-style: none; margin: 0; padding: 0; max-height: 320px; overflow: auto; outline: none; }
.home-menu__head {
padding: 8px 10px 4px;
color: var(--dk-muted);
font-size: 11px;
font-weight: 600;
letter-spacing: .09em;
text-transform: uppercase;
}
.home-menu__empty { padding: 12px 10px; color: var(--dk-muted); font-size: 13px; }
.home-menu__item {
display: flex;
align-items: center;
gap: 12px;
min-height: 40px;
padding: 6px 10px;
border-radius: var(--dk-radius-1);
color: var(--color-text-secondary);
cursor: pointer;
}
.home-menu__item[data-active="true"] { background: var(--dk-hover); color: var(--dk-text); }
.home-menu__item code { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--dk-text); background: none; padding: 0; white-space: nowrap; }
.home-menu__item small { margin-left: auto; font-family: var(--font-mono); font-size: 12px; color: var(--dk-muted); white-space: nowrap; }
.home-menu__check { margin-left: auto; color: var(--dk-accent-text); }
.dk-cmdlist.home-slash {
position: absolute;
z-index: 130;
top: calc(100% + 6px);
left: 0;
right: 0;
width: auto;
max-height: 360px;
animation: homeRise var(--dk-dur-2) var(--dk-ease-out) both;
}
.dk-cmdlist.home-slash { overflow: hidden; padding: 6px 6px 0; }
.home-slash__scroll { display: flex; flex-direction: column; gap: 2px; max-height: 300px; overflow: auto; overscroll-behavior: contain; }
.home-slash .dk-cmd[aria-selected="true"] { background: var(--dk-hover); }
.home-slash .dk-cmd-foot { margin-top: 2px; padding-bottom: 8px; }
.home-well {
position: relative;
border-radius: var(--dk-radius-2);
background: var(--dk-inset);
box-shadow: inset 0 0 0 1px var(--dk-edge);
}
.home-textarea {
display: block;
width: 100%;
min-height: 88px;
padding: 14px 16px;
border: 0;
outline: 0;
resize: none;
background: none;
color: var(--dk-text);
font: inherit;
font-size: 16px;
line-height: 1.5;
}
.home-textarea::placeholder { color: var(--dk-muted); }
.home-file-tags { display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 10px; }
.home-file-tag {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 100%;
padding: 4px 8px;
border-radius: 6px;
background: var(--dk-inset);
box-shadow: inset 0 0 0 1px var(--dk-edge);
color: var(--color-text-secondary);
font-size: 12px;
}
.home-file-tag__thumb { width: 20px; height: 20px; border-radius: 4px; object-fit: cover; }
.home-file-tag__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.home-file-tag button {
display: inline-grid;
place-items: center;
padding: 2px;
border: 0;
border-radius: 4px;
background: none;
color: var(--dk-muted);
cursor: pointer;
}
.home-file-tag button:hover { background: var(--dk-hover); color: var(--dk-text); }
.home-cmd__foot { display: flex; align-items: center; gap: 4px; padding-top: 10px; }
.home-attach-buttons { display: flex; gap: 2px; }
.home-attach-btn {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border: 0;
border-radius: var(--dk-radius-1);
background: none;
color: var(--color-text-secondary);
font-size: 18px;
cursor: pointer;
transition: background var(--dk-dur-1) var(--dk-ease-out), color var(--dk-dur-1) var(--dk-ease-out), transform var(--dk-dur-1) var(--dk-ease-out);
}
.home-attach-btn:hover { background: var(--dk-hover); color: var(--dk-text); }
.home-attach-btn:active { transform: scale(var(--dk-press-scale)); }
.home-input-hint { flex: 1; margin-right: 12px; color: var(--dk-muted); font-size: 13px; text-align: right; }
/* Send: a solid fill, a top highlight and a bottom edge (no glow), and the
Enter glyph in a darker cell that teaches the shortcut. */
.home-send-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
height: 40px;
margin-left: auto;
padding: 0 6px 0 16px;
border: 0;
border-radius: var(--dk-radius-2);
background: var(--dk-accent);
color: var(--dk-on-accent);
font: inherit;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
box-shadow: inset 0 1px 0 rgb(255 255 255 / .18), inset 0 -1px 0 rgb(0 0 0 / .2), var(--dk-shadow-rest);
transition: background var(--dk-dur-1) var(--dk-ease-out), box-shadow var(--dk-dur-1) var(--dk-ease-out), transform var(--dk-dur-1) var(--dk-ease-out);
}
.home-send-btn__cell {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 8px;
background: rgb(0 0 0 / .18);
}
.home-send-btn__glyph {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform var(--dk-dur-2) var(--dk-ease-out);
}
.home-send-btn:hover:not(:disabled) { background: color-mix(in srgb, var(--dk-accent) 88%, white); }
.home-send-btn:hover:not(:disabled) .home-send-btn__glyph { transform: translateX(-2px); }
.home-send-btn:active:not(:disabled) {
transform: scale(var(--dk-press-scale));
background: color-mix(in srgb, var(--dk-accent) 82%, black);
box-shadow: inset 0 1px 2px rgb(0 0 0 / .3);
}
.home-send-btn:focus-visible { outline: 2px solid var(--dk-accent-text); outline-offset: 2px; }
.home-send-btn[data-empty],
.home-send-btn:disabled {
background: var(--dk-inset);
color: var(--dk-muted);
box-shadow: inset 0 0 0 1px var(--dk-edge);
cursor: not-allowed;
}
.home-send-btn[data-empty] .home-send-btn__cell,
.home-send-btn:disabled .home-send-btn__cell { background: transparent; }
.home-send-btn[data-loading] { pointer-events: none; }
.home-send-btn[data-loading] > * { visibility: hidden; }
.home-send-btn[data-loading]::after {
content: "";
position: absolute;
inset: 0;
width: 16px;
height: 16px;
margin: auto;
border-radius: 50%;
border: 2px solid currentColor;
border-right-color: transparent;
animation: dk-spin 700ms linear infinite;
}
/* Under the command bar: notices, the memory strip, the assistant line */
.home-below { display: flex; flex-direction: column; gap: 12px; margin-top: 12px; }
.home-notice {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 14px;
padding: 16px;
border-radius: var(--dk-radius-2);
background: var(--dk-error-wash);
color: var(--dk-text);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dk-error) 40%, transparent);
}
.home-notice > .lai-icon { margin-top: 2px; color: var(--dk-error-text); }
.home-notice h3 { margin: 0; font-size: 15px; font-weight: 650; }
.home-notice p { grid-column: 2; margin: 0; color: var(--color-text-secondary); font-size: 14px; }
.home-notice__acts { grid-column: 2; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.home-notice details { grid-column: 2; margin-top: 6px; font-size: 13px; color: var(--color-text-secondary); }
.home-notice summary { width: max-content; border-radius: 6px; cursor: pointer; }
.home-notice pre {
margin: 8px 0 0;
padding: 10px 12px;
border-radius: var(--dk-radius-1);
background: var(--dk-inset);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.home-tip {
display: flex;
align-items: center;
gap: 10px;
padding: 0 4px;
color: var(--dk-muted);
font-size: 13px;
}
.home-tip b { color: var(--color-text-secondary); font-weight: 600; }
.home-tip > .lai-icon { flex: none; width: 16px; height: 16px; color: var(--dk-accent-text); }
.home-tip__close {
display: grid;
place-items: center;
flex: none;
width: 32px;
height: 32px;
margin-left: auto;
border: 0;
border-radius: var(--dk-radius-1);
background: none;
color: var(--color-text-secondary);
cursor: pointer;
}
.home-tip__close:hover { background: var(--dk-hover); color: var(--dk-text); }
/* Memory strip */
.home-strip {
position: relative;
border-radius: var(--dk-radius-2);
background: var(--dk-card);
box-shadow: inset 0 0 0 1px var(--dk-edge);
}
.home-strip__head {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
min-height: 46px;
padding: 0 14px;
border: 0;
border-radius: var(--dk-radius-2);
background: none;
color: var(--color-text-secondary);
font: inherit;
font-size: 14px;
text-align: left;
cursor: pointer;
transition: background var(--dk-dur-1) var(--dk-ease-out);
}
button.home-strip__head:hover:not(:disabled) { background: var(--dk-hover); }
.home-strip__head--static { padding-top: 12px; padding-bottom: 12px; cursor: default; color: var(--dk-muted); }
.home-strip__head.home-strip__head--static > span:not([class]) { white-space: normal; }
.home-strip__head:disabled { cursor: default; }
.home-strip__head b { color: var(--dk-text); font-weight: 650; }
.home-strip__head > span:not([class]) { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.home-strip__head > .home-bar { flex: 1; min-width: 70px; max-width: 340px; }
.home-strip__caret { margin-left: auto; flex: none; color: var(--dk-muted); transition: transform var(--dk-dur-2) var(--dk-ease-out); }
.home-strip[data-open] .home-strip__caret { transform: rotate(180deg); }
.home-strip[data-mode="error"] .home-strip__head > .lai-icon:first-child { color: var(--dk-error-text); }
.home-strip--skeleton { padding: 16px 14px; }
.home-strip--skeleton .skeleton { height: 12px; margin: 0; background: var(--dk-hover); animation: homePulse 1.4s ease-in-out infinite; }
.home-bar {
position: relative;
display: block;
height: 10px;
padding: 1.5px;
border-radius: 5px;
background: var(--dk-inset);
box-shadow: inset 0 0 0 1px var(--dk-control-edge);
overflow: hidden;
}
.home-bar--thin { height: 8px; padding: 1px; border-radius: 4px; }
.home-bar__fill {
display: block;
width: var(--home-fill, 0%);
height: 100%;
border-radius: 3px;
background: var(--dk-accent);
transition: width var(--dk-dur-3) var(--dk-ease-out);
}
.home-bar[data-level="warn"] .home-bar__fill { background: var(--dk-warn); }
.home-fig { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--dk-text); white-space: nowrap; font-variant-numeric: tabular-nums; }
.home-fig span { color: var(--dk-muted); font-weight: 500; }
.home-device {
max-width: 200px;
padding: 3px 8px;
border-radius: 6px;
box-shadow: inset 0 0 0 1px var(--dk-edge);
color: var(--dk-muted);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.home-mini { display: flex; align-items: center; gap: 14px; margin-left: auto; margin-right: 10px; }
.home-mini > span { display: flex; align-items: center; gap: 6px; color: var(--dk-muted); font-family: var(--font-mono); font-size: 11px; font-weight: 500; }
.home-mini .home-bar { width: 76px; flex: none; }
.home-mini__name { max-width: 80px; overflow: hidden; text-overflow: ellipsis; }
.home-strip__body {
padding: 10px 14px 14px;
border-top: 1px solid var(--dk-edge);
animation: homeRise var(--dk-dur-2) var(--dk-ease-out) both;
}
.home-loaded { list-style: none; margin: 0; padding: 0; }
.home-loaded__row {
display: grid;
grid-template-columns: 12px minmax(0, 1fr) 28px;
align-items: center;
gap: 10px;
min-height: 40px;
margin: 0 -8px;
padding: 0 4px 0 8px;
border-radius: var(--dk-radius-1);
transition: background var(--dk-dur-1) var(--dk-ease-out);
}
.home-loaded__row:hover,
.home-loaded__row:focus-within { background: var(--dk-hover); }
.home-loaded__name { min-width: 0; }
.home-loaded__name code { display: block; padding: 0; background: none; font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--dk-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.home-loaded__name small { display: block; font-family: var(--font-mono); font-size: 11px; line-height: 1.3; color: var(--dk-muted); }
.home-loaded__stop {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border: 0;
border-radius: var(--dk-radius-1);
background: none;
color: var(--dk-muted);
opacity: 0;
cursor: pointer;
transition: opacity var(--dk-dur-1) var(--dk-ease-out), background var(--dk-dur-1) var(--dk-ease-out), color var(--dk-dur-1) var(--dk-ease-out);
}
.home-loaded__row:hover .home-loaded__stop,
.home-loaded__row:focus-within .home-loaded__stop,
.home-loaded__stop:focus-visible { opacity: 1; }
.home-loaded__stop:hover { background: var(--dk-error-wash); color: var(--dk-error-text); }
@media (hover: none) { .home-loaded__stop { opacity: 1; } }
.home-loaded__empty { margin: 0; padding: 4px 0; color: var(--dk-muted); font-size: 14px; }
.home-nodes { list-style: none; margin: 0 0 10px; padding: 0 0 10px; border-bottom: 1px solid var(--dk-edge); }
.home-nodes li { display: grid; grid-template-columns: 12px minmax(0, 8rem) minmax(0, 1fr) auto; align-items: center; gap: 10px; min-height: 36px; }
.home-nodes__name { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--dk-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.home-ram {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 12px;
margin-top: 10px;
padding-top: 12px;
border-top: 1px solid var(--dk-edge);
color: var(--color-text-secondary);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 500;
}
.home-strip__foot { display: flex; justify-content: flex-end; margin-top: 8px; }
/* Jump back in */
.home-resume { margin-top: 36px; }
.home-resume__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding-bottom: 4px; }
.home-resume__head h2 { margin: 0; font-size: 15px; font-weight: 650; letter-spacing: 0; }
.home-resume__keys { display: inline-flex; align-items: center; gap: 4px; color: var(--dk-muted); font-size: 12px; }
.home-day__label {
display: flex;
align-items: baseline;
gap: 10px;
margin: 0;
padding: 18px 4px 8px;
color: var(--dk-muted);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 600;
letter-spacing: .09em;
text-transform: uppercase;
}
.home-day__label span { font-family: var(--font-mono); font-size: 12px; font-weight: 500; letter-spacing: 0; text-transform: none; }
.home-day__card {
list-style: none;
margin: 0;
padding: 0;
overflow: hidden;
border-radius: var(--dk-radius-3);
background: var(--dk-card);
box-shadow: inset 0 0 0 1px var(--dk-edge);
}
.home-row { position: relative; }
.home-row + .home-row { box-shadow: inset 0 1px 0 var(--dk-edge); }
.home-row__open {
display: grid;
grid-template-columns: 48px minmax(0, 1fr) auto 118px;
align-items: center;
gap: 14px;
width: 100%;
min-height: 64px;
padding: 10px 12px 10px 16px;
border: 0;
background: none;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
transition: background var(--dk-dur-1) var(--dk-ease-out), opacity 180ms var(--dk-ease-out), transform 180ms var(--dk-ease-out);
}
.home-row:hover .home-row__open,
.home-row:focus-within .home-row__open { background: var(--dk-hover); }
.home-row .home-row__open:focus-visible { outline-offset: -2px; border-radius: 0; }
.home-row--leaving .home-row__open { opacity: 0; transform: translateX(12px); pointer-events: none; }
.home-row__time { color: var(--dk-muted); font-family: var(--font-mono); font-size: 12px; font-weight: 500; }
.home-row__main { min-width: 0; display: flex; flex-direction: column; }
.home-row__title { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: var(--dk-text); }
.home-row__reply { display: flex; align-items: center; gap: 6px; overflow: hidden; color: var(--dk-muted); font-size: 13px; white-space: nowrap; }
.home-row__reply .lai-icon { flex: none; width: 14px; height: 14px; }
.home-row__reply span { overflow: hidden; text-overflow: ellipsis; }
.home-row__model {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 240px;
padding: 3px 8px;
border-radius: 6px;
background: var(--dk-inset);
box-shadow: inset 0 0 0 1px var(--dk-edge);
color: var(--color-text-secondary);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.home-row__model .lai-icon { flex: none; width: 13px; height: 13px; }
.home-row__end {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 2px;
min-height: 32px;
color: var(--dk-muted);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.home-row__resume { display: none; align-items: center; gap: 6px; }
.home-row:hover .home-row__count,
.home-row:focus-within .home-row__count { display: none; }
.home-row:hover .home-row__resume,
.home-row:focus-within .home-row__resume { display: inline-flex; }
.home-row:hover .home-row__end,
.home-row:focus-within .home-row__end { padding-right: 36px; }
.home-row__delete {
position: absolute;
top: 50%;
right: 12px;
display: grid;
place-items: center;
width: 32px;
height: 32px;
margin-top: -16px;
border: 0;
border-radius: var(--dk-radius-1);
background: none;
color: var(--color-text-secondary);
font-size: 18px;
opacity: 0;
pointer-events: none;
cursor: pointer;
transition: opacity var(--dk-dur-1) var(--dk-ease-out), background var(--dk-dur-1) var(--dk-ease-out), color var(--dk-dur-1) var(--dk-ease-out);
}
.home-row:hover .home-row__delete,
.home-row:focus-within .home-row__delete { opacity: 1; pointer-events: auto; }
.home-row__delete:hover { background: var(--dk-error-wash); color: var(--dk-error-text); }
.home-more {
display: block;
margin: 12px 0 0 -4px;
padding: 6px 8px;
border: 0;
border-radius: var(--dk-radius-1);
background: none;
color: var(--color-text-secondary);
font: inherit;
font-size: 13px;
font-weight: 550;
cursor: pointer;
}
.home-more:hover { background: var(--dk-hover); color: var(--dk-text); }
.home-empty {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 8px;
padding: 20px;
border-radius: var(--dk-radius-3);
background: var(--dk-inset);
box-shadow: inset 0 0 0 1px var(--dk-edge);
color: var(--color-text-secondary);
font-size: 14px;
}
.home-empty p { margin: 0; }
.home-empty b { color: var(--dk-text); }
.home-examples { display: flex; flex-wrap: wrap; gap: 8px; }
.home-examples code {
padding: 3px 8px;
border-radius: 6px;
background: var(--dk-card);
box-shadow: inset 0 0 0 1px var(--dk-edge);
color: var(--color-text-secondary);
font-family: var(--font-mono);
font-size: 12px;
}
.home-libline {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-top: 32px;
color: var(--dk-muted);
font-size: 13px;
}
.home-libline .home-ghost { min-height: 30px; padding: 0 9px; }
/* First run */
.home-first { margin-top: 16px; }
.home-guide {
display: flex;
flex-direction: column;
list-style: none;
margin: 0;
padding: 0;
overflow: hidden;
border-radius: var(--dk-radius-3);
background: var(--dk-card);
box-shadow: inset 0 0 0 1px var(--dk-edge);
}
.home-step {
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
gap: 4px 14px;
padding: 16px 18px;
color: var(--dk-muted);
}
.home-step + .home-step { border-top: 1px solid var(--dk-edge); }
.home-step[data-state="now"] { background: color-mix(in srgb, var(--dk-card) 94%, var(--dk-text)); color: var(--dk-text); }
.home-step__num {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 50%;
box-shadow: inset 0 0 0 1.75px var(--dk-control-edge);
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
}
.home-step[data-state="now"] .home-step__num { box-shadow: inset 0 0 0 1.75px var(--dk-accent-text); color: var(--dk-accent-text); }
.home-step__main { min-width: 0; }
.home-step__main > b { display: block; font-size: 15px; font-weight: 600; color: var(--dk-text); }
.home-step[data-state="now"] .home-step__main > b { font-weight: 700; }
.home-step__desc { display: block; margin-top: 2px; font-size: 13px; line-height: 1.45; }
.home-step__extra { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 10px; }
.home-step__extra > .home-starters { flex: 1 1 100%; }
.home-starters-head { display: flex; align-items: center; gap: 8px; }
.home-starters-title { color: var(--color-text-secondary); font-size: 13px; }
.home-starters-tier {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 7px;
border-radius: 99px;
background: var(--dk-accent-wash);
color: var(--dk-accent-text);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
}
.home-starters-sub { margin: 2px 0 4px; color: var(--dk-muted); font-size: 13px; }
.home-starters-list { list-style: none; margin: 0; padding: 0; }
.home-starters-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 4px 12px;
padding: 10px 0;
border-top: 1px solid var(--dk-edge);
}
.home-starters-item:first-child { border-top: 0; }
.home-starters-info { min-width: 0; }
.home-starters-name { display: block; padding: 0; background: none; color: var(--dk-text); font-family: var(--font-mono); font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.home-starters-meta { display: flex; align-items: center; gap: 6px; color: var(--dk-muted); font-family: var(--font-mono); font-size: 12px; font-weight: 500; }
.home-starters-badge { padding: 2px 6px; border-radius: 5px; background: var(--dk-accent-wash); color: var(--dk-accent-text); font-size: 10px; font-weight: 600; }
.home-starters-progress { grid-column: 1 / -1; }
/* Non-admin, nothing installed */
.home-wizard { display: flex; flex-direction: column; gap: var(--spacing-lg); width: 100%; max-width: 560px; margin: auto; }
.home-wizard-hero { text-align: center; padding: var(--spacing-sm) 0 var(--spacing-md); }
.home-wizard-hero h1 { margin: var(--spacing-md) 0 var(--spacing-xs); font-size: 24px; font-weight: 650; letter-spacing: -0.02em; }
.home-wizard-hero p { margin: 0; color: var(--dk-muted); }
.home-wizard-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
.home-logo { display: block; width: 72px; height: auto; margin: 0 auto; }
/* One endpoint, every API: a collapsed row */
.home-connect { margin-top: 32px; border-top: 1px solid var(--dk-edge); }
.home-connect__head {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
min-height: 52px;
padding: 0;
border: 0;
border-radius: var(--dk-radius-1);
background: none;
color: var(--color-text-secondary);
font: inherit;
text-align: left;
cursor: pointer;
}
.home-connect__head b { flex: none; color: var(--dk-text); font-size: 15px; font-weight: 650; }
.home-connect__base { min-width: 0; padding: 0; overflow: hidden; background: none; color: var(--dk-muted); font-family: var(--font-mono); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.home-connect__compat { margin-left: auto; flex: none; color: var(--dk-muted); font-size: 13px; }
.home-connect__caret { flex: none; color: var(--dk-muted); transition: transform var(--dk-dur-2) var(--dk-ease-out); }
.home-connect[data-open] .home-connect__caret { transform: rotate(180deg); }
.home-connect__body { padding: 4px 0 8px; animation: homeRise var(--dk-dur-2) var(--dk-ease-out) both; }
.home-connect__sub { margin: 0 0 12px; color: var(--dk-muted); font-size: 13px; line-height: 1.5; }
.home-connect-url {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 6px 6px 14px;
border-radius: var(--dk-radius-2);
background: var(--dk-inset);
box-shadow: inset 0 0 0 1px var(--dk-edge);
}
.home-connect-url code { flex: 1; min-width: 0; padding: 0; overflow: hidden; background: none; font-family: var(--font-mono); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
.home-connect-block { margin-top: 16px; }
.home-connect-block-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.home-connect-block-title { color: var(--dk-muted); font-size: 11px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; }
.home-connect-docs { display: inline-flex; align-items: center; gap: 4px; color: var(--dk-accent-text); font-size: 13px; font-weight: 550; text-decoration: none; }
.home-connect-docs:hover { text-decoration: underline; text-underline-offset: 3px; }
.home-connect-apis { list-style: none; margin: 6px 0 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0 16px; }
.home-connect-api { display: flex; justify-content: space-between; gap: 8px; padding: 6px 0; font-size: 13px; }
.home-connect-api-name { color: var(--color-text-secondary); font-weight: 500; }
.home-connect-api-path { padding: 0; background: none; color: var(--dk-muted); font-family: var(--font-mono); font-size: 12px; }
.home-connect__foot { display: flex; justify-content: flex-end; margin-top: 12px; }
@keyframes homeRise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes homePulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
@media (max-width: 640px) {
.home-page { padding: var(--spacing-sm) 16px 64px; }
.home-greeting { font-size: 24px; }
.home-cmd { padding: 10px; }
.home-slash-hint,
.home-input-hint,
.home-resume__keys,
.home-connect__compat,
.home-connect__base { display: none; }
.home-strip__head > .home-bar,
.home-mini { display: none; }
/* The sentence ("2 models loaded") is the point of the strip, so it gets the
first line to itself and may wrap; the memory figure and device wrap onto
a second line instead of squeezing it into an ellipsis. */
.home-strip__head { gap: 4px 10px; flex-wrap: wrap; padding-block: 10px; }
.home-strip__head > span:not([class]) { flex: 1 1 calc(100% - 56px); overflow: visible; white-space: normal; text-overflow: clip; }
.home-strip__head > .home-strip__caret { order: 1; margin-left: 0; }
.home-strip__head > .home-fig,
.home-strip__head > .home-device { order: 2; }
.home-strip__head > .home-fig { margin-left: 18px; }
.home-strip__head > .home-device { max-width: 100%; flex: 0 1 auto; min-width: 0; }
.home-row__open { grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; padding: 10px 12px; }
.home-row__time { display: none; }
.home-row__model { grid-column: 1; grid-row: 2; justify-self: start; margin-top: 4px; max-width: 100%; }
.home-row__end { grid-column: 2; grid-row: 1; padding-right: 36px; }
.home-row__resume { display: none !important; }
.home-row:hover .home-row__count,
.home-row:focus-within .home-row__count { display: inline; }
.home-row__delete { opacity: 1; pointer-events: auto; top: 22px; margin-top: -16px; right: 6px; }
.home-menu { min-width: 0; width: calc(100vw - 48px); }
.home-nodes li { grid-template-columns: 12px minmax(0, 1fr) auto; }
.home-nodes .home-bar { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.home-menu, .dk-cmdlist.home-slash, .home-strip__body, .home-connect__body { animation: none; }
.home-row__open, .home-row--leaving .home-row__open { transition: background var(--dk-dur-1) var(--dk-ease-out); }
.home-row--leaving .home-row__open { transform: none; }
.home-send-btn:active:not(:disabled),
.home-chip:active:not(:disabled),
.home-ghost:active,
.home-primary:active:not(:disabled),
.home-secondary:active:not(:disabled),
.home-attach-btn:active { transform: none; }
.home-send-btn:hover:not(:disabled) .home-send-btn__glyph { transform: none; }
.home-strip--skeleton .skeleton { animation: none; }
}
/* ──────────────────── Models gallery: recommended-for-your-hardware strip ──────────────────── */
.rec-models {
/* A section in the flow, not a card on top of it. */
background: transparent;
border: 0;
padding: 0;
}
.rec-models__head { margin-bottom: var(--spacing-xs); }
.rec-models__note {
margin: 2px 0 0;
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.rec-models__facts > span + span::before { content: "\00b7"; margin-right: var(--spacing-sm); }
.rec-models__facts { display: flex; flex-wrap: wrap; align-items: center; gap: 2px var(--spacing-sm); overflow: visible; }
.rec-models__more {
margin-top: var(--spacing-xs);
padding: var(--spacing-xs) 0;
border: 0;
background: transparent;
color: var(--color-text-secondary);
font: inherit;
font-size: var(--text-sm);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
}
.rec-models__more:hover { color: var(--color-text-primary); }
.rec-models__more:focus-visible { outline: 2px solid var(--color-focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
/* ──────────────────── Biometrics (face + voice recognition) ──────────────────── */
.biometrics-page {
padding: var(--spacing-xl);
max-width: var(--page-max-wide);
margin: 0 auto;
width: 100%;
animation: fadeIn var(--duration-normal) var(--ease-default);
}
.biometrics-page__header {
display: grid;
grid-template-columns: 1fr minmax(240px, 320px);
gap: var(--spacing-lg);
align-items: end;
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--color-border-divider);
}
.biometrics-page__header .page-title .lai-icon {
color: var(--color-accent);
}
.biometrics-page__model {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.biometrics-page__body {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
min-width: 0;
}
@media (max-width: 720px) {
.biometrics-page__header {
grid-template-columns: 1fr;
align-items: stretch;
}
}
/* Tabs — flat, underlined, inherit page tone */
.biometrics-tabs {
display: flex;
gap: var(--spacing-xs);
border-bottom: 1px solid var(--color-border-subtle);
overflow-x: auto;
scrollbar-width: none;
}
.biometrics-tabs::-webkit-scrollbar { display: none; }
.biometrics-tab {
background: transparent;
border: 0;
padding: var(--spacing-sm) var(--spacing-md);
color: var(--color-text-secondary);
font: inherit;
font-weight: var(--font-weight-medium);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
border-bottom: 2px solid transparent;
min-height: 44px;
transition: color var(--duration-fast), border-color var(--duration-fast);
white-space: nowrap;
}
.biometrics-tab:hover { color: var(--color-text-primary); }
.biometrics-tab.active {
color: var(--color-text-primary);
border-bottom-color: var(--color-accent);
}
.biometrics-tab .lai-icon { color: var(--color-accent); font-size: 0.9em; }
/* Two-column workflow layout */
.biometrics-twocol {
display: grid;
grid-template-columns: minmax(300px, 380px) 1fr;
gap: var(--spacing-lg);
align-items: start;
min-width: 0;
}
@media (max-width: 980px) {
.biometrics-twocol { grid-template-columns: 1fr; }
}
.biometrics-panel {
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow-subtle), var(--shadow-inset-top);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.biometrics-panel__title {
font-size: var(--text-lg);
font-weight: var(--font-weight-semibold);
margin: 0;
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.biometrics-panel__title .lai-icon { color: var(--color-accent); }
.biometrics-panel__note {
margin: 0;
font-size: var(--text-sm);
color: var(--color-text-secondary);
line-height: var(--leading-normal);
}
.biometrics-results {
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.biometrics-empty {
background: var(--color-bg-secondary);
border: 1px dashed var(--color-border-default);
border-radius: var(--radius-lg);
padding: var(--spacing-2xl) var(--spacing-lg);
text-align: center;
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
color: var(--color-text-secondary);
}
.biometrics-empty > .lai-icon {
font-size: 2.5rem;
color: var(--color-accent);
opacity: 0.6;
}
.biometrics-empty h3 {
margin: 0;
font-size: var(--text-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.biometrics-empty p {
margin: 0;
max-width: 48ch;
line-height: var(--leading-normal);
font-size: var(--text-sm);
}
/* Media input — file / webcam / record switcher */
.biometrics-mediainput {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.biometrics-mediainput__tabs {
display: inline-flex;
gap: 2px;
padding: 2px;
background: var(--color-bg-tertiary);
border-radius: var(--radius-md);
align-self: flex-start;
}
.biometrics-mediainput__tab {
background: transparent;
border: 0;
font: inherit;
color: var(--color-text-secondary);
padding: 6px 12px;
min-height: 32px;
border-radius: var(--radius-sm);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
transition: background var(--duration-fast), color var(--duration-fast);
}
.biometrics-mediainput__tab:hover:not(:disabled) { color: var(--color-text-primary); }
.biometrics-mediainput__tab.active {
background: var(--color-surface-raised);
color: var(--color-text-primary);
box-shadow: var(--shadow-subtle);
}
.biometrics-mediainput__tab:disabled { opacity: 0.4; cursor: not-allowed; }
.biometrics-mediainput__body {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.biometrics-mediainput__live {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.biometrics-mediainput__video {
width: 100%;
aspect-ratio: 4 / 3;
border-radius: var(--radius-md);
background: var(--color-surface-sunken);
object-fit: cover;
}
.biometrics-mediainput__controls {
display: flex;
gap: var(--spacing-xs);
}
.biometrics-mediainput__controls .btn { flex: 1; min-height: 40px; }
.biometrics-mediainput__meter {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
font-size: var(--text-sm);
font-variant-numeric: tabular-nums;
}
.biometrics-mediainput__meter .lai-icon { color: var(--color-text-muted); }
.biometrics-mediainput__meter.recording {
border-color: var(--color-error-border);
color: var(--color-text-primary);
}
.biometrics-mediainput__meter.recording .lai-icon {
color: var(--color-error);
animation: biometrics-pulse 1.2s ease-in-out infinite;
}
@keyframes biometrics-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.biometrics-mediainput__error {
margin: 0;
color: var(--color-error);
font-size: var(--text-sm);
}
.biometrics-mediainput__notice {
display: flex;
gap: var(--spacing-sm);
align-items: flex-start;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-warning-light);
border: 1px solid var(--color-warning-border);
border-radius: var(--radius-md);
color: var(--color-text-primary);
font-size: var(--text-sm);
line-height: var(--leading-normal);
}
.biometrics-mediainput__notice > .lai-icon {
color: var(--color-warning);
margin-top: 3px;
flex-shrink: 0;
}
.biometrics-mediainput__notice strong {
display: block;
margin-bottom: 2px;
}
.biometrics-mediainput__notice p {
margin: 0;
color: var(--color-text-secondary);
font-size: var(--text-xs);
}
.biometrics-mediainput__notice code {
background: var(--color-bg-tertiary);
padding: 1px 6px;
border-radius: var(--radius-sm);
font-size: 0.95em;
}
.biometrics-mediainput__preview {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
padding: var(--spacing-sm);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
}
.biometrics-mediainput__preview img {
width: 100%;
max-height: 220px;
object-fit: contain;
border-radius: var(--radius-sm);
background: var(--color-surface-sunken);
}
.biometrics-mediainput__preview audio { width: 100%; }
.biometrics-mediainput__preview-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-xs);
}
.biometrics-mediainput__source-pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: var(--text-xs);
color: var(--color-text-muted);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.biometrics-mediainput__clear {
background: transparent;
border: 0;
color: var(--color-text-muted);
cursor: pointer;
min-width: 32px;
min-height: 32px;
border-radius: var(--radius-sm);
transition: color var(--duration-fast), background var(--duration-fast);
}
.biometrics-mediainput__clear:hover {
color: var(--color-error);
background: var(--color-error-light);
}
/* Fieldsets + chip toggles (attribute actions) */
.biometrics-fieldset {
border: 0;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.biometrics-fieldset legend {
font-size: var(--text-xs);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0;
margin: 0;
}
.biometrics-chipset {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.biometrics-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
font-size: var(--text-xs);
color: var(--color-text-secondary);
cursor: pointer;
text-transform: capitalize;
transition: border-color var(--duration-fast), color var(--duration-fast), background var(--duration-fast);
min-height: 32px;
}
.biometrics-chip input { position: absolute; opacity: 0; pointer-events: none; }
.biometrics-chip:hover { color: var(--color-text-primary); }
.biometrics-chip.active {
border-color: var(--color-accent-border);
background: var(--color-accent-light);
color: var(--color-text-primary);
}
/* Toggle switch */
.biometrics-switch {
display: inline-block;
position: relative;
width: 40px;
height: 22px;
flex-shrink: 0;
}
.biometrics-switch input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.biometrics-switch > span {
position: absolute;
inset: 0;
background: var(--color-toggle-off);
border-radius: var(--radius-full);
transition: background var(--duration-fast);
cursor: pointer;
}
.biometrics-switch > span::after {
content: "";
position: absolute;
left: 2px;
top: 2px;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--color-bg-secondary);
transition: transform var(--duration-fast);
box-shadow: var(--shadow-subtle);
}
.biometrics-switch input:checked + span { background: var(--color-accent); }
.biometrics-switch input:checked + span::after { transform: translateX(18px); }
.biometrics-switch input:focus-visible + span {
outline: 2px solid var(--color-border-focus);
outline-offset: 2px;
}
/* Split view for analyze (image + summary side) */
.biometrics-split {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 1fr);
gap: var(--spacing-md);
align-items: start;
}
@media (max-width: 980px) {
.biometrics-split { grid-template-columns: 1fr; }
}
.biometrics-split__media {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.biometrics-split__aside {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
min-width: 0;
}
/* Bounding box overlay */
.biometrics-bbox {
position: relative;
display: inline-block;
width: 100%;
max-width: 100%;
border-radius: var(--radius-md);
background: var(--color-surface-sunken);
overflow: hidden;
line-height: 0;
}
.biometrics-bbox img {
width: 100%;
height: auto;
display: block;
}
.biometrics-bbox__box {
position: absolute;
border: 2px solid var(--color-accent);
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
pointer-events: none;
transition: border-color var(--duration-fast);
}
.biometrics-bbox__box.tone-default { border-color: var(--color-border-strong); box-shadow: none; }
.biometrics-bbox__box.tone-success { border-color: var(--color-success); }
.biometrics-bbox__box.tone-error { border-color: var(--color-error); }
.biometrics-bbox__box.tone-warning { border-color: var(--color-warning); }
.biometrics-bbox__tag {
position: absolute;
left: -2px;
top: -2px;
transform: translateY(-100%);
background: var(--color-bg-overlay);
border: 1px solid var(--color-border-subtle);
border-bottom: 0;
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
padding: 2px 8px;
font-size: var(--text-xs);
color: var(--color-text-primary);
display: inline-flex;
gap: 6px;
white-space: nowrap;
line-height: var(--leading-snug);
}
.biometrics-bbox__tag strong { font-weight: var(--font-weight-semibold); }
.biometrics-bbox__tag span { color: var(--color-text-secondary); }
.biometrics-facepicker {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.biometrics-facepicker__chip {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-subtle);
color: var(--color-text-secondary);
padding: 4px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: var(--text-xs);
font: inherit;
font-size: var(--text-xs);
min-height: 32px;
transition: border-color var(--duration-fast), color var(--duration-fast), background var(--duration-fast);
}
.biometrics-facepicker__chip:hover { color: var(--color-text-primary); }
.biometrics-facepicker__chip.active {
border-color: var(--color-accent-border);
background: var(--color-accent-light);
color: var(--color-text-primary);
}
.biometrics-facepicker__chip small { margin-left: 4px; color: var(--color-text-muted); }
/* Summary card (dominant attributes) */
.biometrics-summary {
padding: var(--spacing-md);
}
.biometrics-summary__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
.biometrics-summary__head h3 {
font-size: var(--text-base);
margin: 0;
font-weight: var(--font-weight-semibold);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.biometrics-summary__head h3 .lai-icon { color: var(--color-accent); }
.biometrics-summary__head h3 small {
color: var(--color-text-muted);
font-weight: var(--font-weight-regular);
font-size: var(--text-sm);
font-variant-numeric: tabular-nums;
}
.biometrics-summary__grid {
display: grid;
grid-template-columns: max-content 1fr;
column-gap: var(--spacing-md);
row-gap: 6px;
margin: 0;
}
.biometrics-summary__grid dt {
color: var(--color-text-muted);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
align-self: center;
}
.biometrics-summary__grid dd {
margin: 0;
color: var(--color-text-primary);
font-weight: var(--font-weight-medium);
}
/* Distribution bars */
.biometrics-dist {
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.biometrics-dist__head {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.biometrics-dist__head h3 {
font-size: var(--text-sm);
margin: 0;
font-weight: var(--font-weight-semibold);
letter-spacing: -0.005em;
}
.biometrics-dist__head .lai-icon { color: var(--color-accent); }
.biometrics-dist__dominant {
margin-left: auto;
font-size: var(--text-xs);
color: var(--color-text-muted);
text-transform: capitalize;
}
.biometrics-dist__rows {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.biometrics-dist__row {
display: grid;
grid-template-columns: minmax(80px, 110px) 1fr max-content;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--text-xs);
}
.biometrics-dist__label {
color: var(--color-text-secondary);
text-transform: capitalize;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.biometrics-dist__bar-wrap {
height: 6px;
background: var(--color-bg-tertiary);
border-radius: var(--radius-full);
overflow: hidden;
}
.biometrics-dist__bar {
height: 100%;
background: var(--color-text-muted);
border-radius: var(--radius-full);
transition: width var(--duration-normal) var(--ease-default);
}
.biometrics-dist__row.dominant .biometrics-dist__label { color: var(--color-text-primary); }
.biometrics-dist__row.dominant .biometrics-dist__bar { background: var(--color-accent); }
.biometrics-dist__value {
font-variant-numeric: tabular-nums;
color: var(--color-text-muted);
font-size: var(--text-xs);
}
.biometrics-dist__row.dominant .biometrics-dist__value { color: var(--color-text-primary); }
/* Pill chips (liveness) */
.biometrics-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: var(--radius-sm);
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
border: 1px solid var(--color-border-subtle);
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
}
.biometrics-pill small {
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
}
.biometrics-pill.good {
background: var(--color-success-light);
border-color: var(--color-success-border);
color: var(--color-success);
}
.biometrics-pill.bad {
background: var(--color-error-light);
border-color: var(--color-error-border);
color: var(--color-error);
}
.biometrics-pill.muted { color: var(--color-text-muted); }
/* Compare view */
.biometrics-compare {
display: grid;
grid-template-columns: 1fr minmax(280px, 360px) 1fr;
gap: var(--spacing-md);
align-items: stretch;
}
@media (max-width: 1080px) {
.biometrics-compare { grid-template-columns: 1fr; }
}
.biometrics-compare__panel {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.biometrics-compare__label {
font-size: var(--text-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-muted);
}
.biometrics-compare__center {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
justify-content: center;
}
.biometrics-compare__threshold {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
}
.biometrics-compare__threshold label {
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
}
.biometrics-compare__threshold code {
color: var(--color-accent);
font-variant-numeric: tabular-nums;
}
.biometrics-compare__threshold input[type="range"] {
width: 100%;
accent-color: var(--color-accent);
}
.biometrics-compare__hint {
margin: 0;
color: var(--color-text-muted);
font-size: var(--text-xs);
}
.biometrics-compare__hint code { color: var(--color-text-secondary); }
/* Match gauge */
.biometrics-gauge {
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
box-shadow: var(--shadow-subtle), var(--shadow-inset-top);
}
.biometrics-gauge__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-sm);
}
.biometrics-gauge__verdict {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: var(--text-lg);
font-weight: var(--font-weight-semibold);
}
.biometrics-gauge.tone-success .biometrics-gauge__verdict { color: var(--color-success); }
.biometrics-gauge.tone-error .biometrics-gauge__verdict { color: var(--color-error); }
.biometrics-gauge__confidence {
text-align: right;
font-variant-numeric: tabular-nums;
line-height: var(--leading-tight);
}
.biometrics-gauge__confidence strong {
display: block;
font-size: var(--text-xl);
color: var(--color-text-primary);
}
.biometrics-gauge__confidence span {
font-size: var(--text-xs);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.biometrics-gauge__track {
position: relative;
height: 18px;
background: var(--color-bg-tertiary);
border-radius: var(--radius-full);
overflow: hidden;
}
.biometrics-gauge__zone {
position: absolute;
top: 0;
bottom: 0;
transition: width var(--duration-normal) var(--ease-default);
}
.biometrics-gauge__zone--match {
left: 0;
background: var(--color-success-light);
border-right: 1px dashed var(--color-success-border);
}
.biometrics-gauge__zone--miss {
background: var(--color-error-light);
}
.biometrics-gauge__threshold {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: var(--color-border-strong);
transform: translateX(-1px);
}
.biometrics-gauge__threshold span {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
font-size: 9px;
text-transform: uppercase;
color: var(--color-text-muted);
letter-spacing: 0.08em;
padding: 1px 4px;
white-space: nowrap;
}
.biometrics-gauge__marker {
position: absolute;
top: -4px;
bottom: -4px;
width: 12px;
transform: translateX(-6px);
background: var(--color-text-primary);
border-radius: 2px;
border: 2px solid var(--color-surface-raised);
transition: left var(--duration-normal) var(--ease-default);
box-shadow: var(--shadow-sm);
}
.biometrics-gauge__marker span {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
font-size: 9px;
text-transform: uppercase;
color: var(--color-text-primary);
letter-spacing: 0.08em;
padding-top: 4px;
white-space: nowrap;
}
.biometrics-gauge__footer {
display: flex;
justify-content: space-between;
gap: var(--spacing-md);
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.biometrics-gauge__footer em {
text-transform: uppercase;
letter-spacing: 0.06em;
font-style: normal;
margin-right: 4px;
}
.biometrics-gauge__footer code {
font-variant-numeric: tabular-nums;
color: var(--color-text-secondary);
}
/* Waveform */
.biometrics-waveform {
--biometrics-wave: var(--color-accent);
position: relative;
width: 100%;
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
}
.biometrics-waveform--error {
padding: var(--spacing-md);
color: var(--color-error);
font-size: var(--text-sm);
}
.biometrics-waveform__segment {
position: absolute;
top: 0;
bottom: 0;
background: color-mix(in srgb, var(--color-accent) 16%, transparent);
border-left: 1px dashed var(--color-accent-border);
border-right: 1px dashed var(--color-accent-border);
pointer-events: none;
}
.biometrics-waveform__segment.tone-info { background: var(--color-info-light); border-color: var(--color-info-border); }
.biometrics-waveform__segment.tone-success { background: var(--color-success-light); border-color: var(--color-success-border); }
.biometrics-waveform__segment.tone-warning { background: var(--color-warning-light); border-color: var(--color-warning-border); }
.biometrics-waveform__segment.tone-accent { background: var(--color-accent-light); border-color: var(--color-accent-border); }
.biometrics-waveform__seglabel {
position: absolute;
top: 4px;
left: 4px;
font-size: var(--text-xs);
color: var(--color-text-primary);
background: var(--color-bg-overlay);
padding: 1px 6px;
border-radius: var(--radius-sm);
max-width: calc(100% - 8px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.biometrics-waveform__duration {
position: absolute;
right: 8px;
bottom: 6px;
font-size: 11px;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
background: var(--color-bg-overlay);
padding: 1px 6px;
border-radius: var(--radius-sm);
}
.biometrics-waveform__loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
font-size: var(--text-sm);
}
/* Reusable waveform-and-playback component (audio transform / TTS / sound / traces) */
.audio-waveform-player {
--audio-wave: var(--color-primary);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
width: 100%;
}
.audio-waveform-player--dimmed .audio-waveform-player__canvas-wrap {
opacity: 0.7;
}
.audio-waveform-player__label {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.audio-waveform-player__canvas-wrap {
position: relative;
width: 100%;
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
}
.audio-waveform-player__error {
padding: var(--spacing-md);
color: var(--color-error);
font-size: var(--text-sm);
}
.audio-waveform-player__segment {
position: absolute;
top: 0;
bottom: 0;
background: color-mix(in srgb, var(--color-primary) 16%, transparent);
border-left: 1px dashed var(--color-primary);
border-right: 1px dashed var(--color-primary);
pointer-events: none;
}
.audio-waveform-player__seglabel {
position: absolute;
top: 4px;
left: 4px;
font-size: var(--text-xs);
color: var(--color-text-primary);
background: var(--color-bg-overlay);
padding: 1px 6px;
border-radius: var(--radius-sm);
max-width: calc(100% - 8px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.audio-waveform-player__duration {
position: absolute;
right: 8px;
bottom: 6px;
font-size: 11px;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
background: var(--color-bg-overlay);
padding: 1px 6px;
border-radius: var(--radius-sm);
}
.audio-waveform-player__loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.audio-waveform-player__playhead {
position: absolute;
top: 0;
bottom: 0;
width: 1.5px;
background: var(--color-primary);
opacity: 0.85;
pointer-events: none;
transform: translateX(-0.75px);
}
.audio-waveform-player__player {
width: 100%;
}
.audio-waveform-player__download {
align-self: flex-end;
font-size: var(--text-sm);
color: var(--color-primary);
}
/* Spectrogram (AudioTransform spectral view) */
.audio-spectrogram-pair {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
}
@media (max-width: 720px) {
.audio-spectrogram-pair {
grid-template-columns: 1fr;
}
}
.audio-spectrogram {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
width: 100%;
min-width: 0;
}
.audio-spectrogram__label {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.audio-spectrogram__canvas-wrap {
position: relative;
width: 100%;
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
}
.audio-spectrogram__canvas-wrap--empty {
display: flex;
align-items: center;
justify-content: center;
}
.audio-spectrogram__hint {
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.audio-spectrogram__loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.audio-spectrogram__error {
padding: var(--spacing-md);
color: var(--color-error);
font-size: var(--text-sm);
}
.audio-spectrogram__axis {
position: absolute;
left: 6px;
font-size: 10px;
color: var(--color-text-muted);
background: var(--color-bg-overlay);
padding: 0 4px;
border-radius: var(--radius-sm);
pointer-events: none;
font-variant-numeric: tabular-nums;
}
.audio-spectrogram__axis--top {
top: 4px;
}
.audio-spectrogram__axis--bottom {
bottom: 4px;
}
.audio-spectrogram__duration {
position: absolute;
right: 8px;
bottom: 6px;
font-size: 11px;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
background: var(--color-bg-overlay);
padding: 1px 6px;
border-radius: var(--radius-sm);
}
/* Audio Transform Studio tab */
.audio-transform-stack {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
width: 100%;
}
.audio-transform-drop {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
border: 1px dashed var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-sunken);
color: var(--color-text-secondary);
cursor: default;
transition: border-color var(--duration-normal, 180ms) var(--ease-default, ease);
}
.audio-transform-drop--hover {
border-color: var(--color-primary);
background: var(--color-primary-light);
}
.audio-transform-drop__file {
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex: 1;
font-family: var(--font-mono);
word-break: break-all;
}
.audio-transform-drop__pick {
cursor: pointer;
color: var(--color-primary);
text-decoration: underline;
margin-left: 4px;
}
.audio-transform-input {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.audio-transform-input__tabs {
display: inline-flex;
gap: 2px;
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: 2px;
align-self: flex-start;
}
.audio-transform-input__tab {
border: 0;
background: transparent;
padding: 4px 10px;
font-size: var(--text-sm);
color: var(--color-text-secondary);
border-radius: var(--radius-sm);
cursor: pointer;
}
.audio-transform-input__tab.active {
background: var(--color-primary-light);
color: var(--color-text-primary);
}
.audio-transform-rec {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
align-items: flex-start;
padding: var(--spacing-md);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
background: var(--color-surface-sunken);
}
.audio-transform-rec__notice {
font-size: var(--text-sm);
color: var(--color-text-secondary);
display: flex;
gap: var(--spacing-xs);
align-items: center;
}
.audio-transform-rec__notice--error {
color: var(--color-error);
}
.audio-transform-rec__pending {
font-size: var(--text-sm);
color: var(--color-text-muted);
font-style: italic;
}
.audio-transform-echo {
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.audio-transform-echo__row {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.audio-transform-echo__notice {
display: flex;
gap: var(--spacing-sm);
align-items: flex-start;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-info-light);
color: var(--color-text-primary);
border: 1px solid var(--color-info-border);
border-radius: var(--radius-md);
font-size: var(--text-sm);
margin: 0;
}
.audio-transform-echo__notice > .lai-icon {
color: var(--color-info);
margin-top: 2px;
}
.audio-transform-echo__elapsed {
font-size: var(--text-sm);
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
}
/* Enrollment layout (register + identify + list) */
.biometrics-enrollgrid {
display: grid;
grid-template-columns: minmax(300px, 1fr) minmax(300px, 1fr);
grid-template-areas:
"register identify"
"list list";
gap: var(--spacing-lg);
}
.biometrics-enrollgrid__register { grid-area: register; }
.biometrics-enrollgrid__identify { grid-area: identify; }
.biometrics-enrollgrid__list { grid-area: list; min-width: 0; }
@media (max-width: 980px) {
.biometrics-enrollgrid {
grid-template-columns: 1fr;
grid-template-areas:
"register"
"identify"
"list";
}
}
.biometrics-enrollgrid__register form,
.biometrics-enrollgrid__identify form {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.biometrics-enrollgrid__err {
margin-top: var(--spacing-sm);
}
.biometrics-enroll__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-md);
}
.biometrics-enroll__count {
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
padding: 2px 8px;
border-radius: var(--radius-sm);
margin-left: var(--spacing-xs);
}
.biometrics-enroll__grid {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: var(--spacing-md);
}
.biometrics-enroll__card {
position: relative;
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
transition: border-color var(--duration-fast), transform var(--duration-fast);
}
.biometrics-enroll__card:hover {
border-color: var(--color-border-default);
transform: translateY(-1px);
}
.biometrics-enroll__card.highlight {
border-color: var(--color-accent-border);
box-shadow: 0 0 0 1px var(--color-accent-border);
animation: biometrics-highlight 1.4s ease-out;
}
@keyframes biometrics-highlight {
0% { box-shadow: 0 0 0 4px var(--color-accent-light); }
100% { box-shadow: 0 0 0 1px var(--color-accent-border); }
}
.biometrics-enroll__media {
aspect-ratio: 1 / 1;
background: var(--color-surface-sunken);
border-radius: var(--radius-md);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.biometrics-enroll__media img {
width: 100%;
height: 100%;
object-fit: cover;
}
.biometrics-enroll__media audio {
width: 90%;
}
.biometrics-enroll__initials {
font-size: 2rem;
font-weight: var(--font-weight-semibold);
color: var(--color-text-muted);
letter-spacing: 0.04em;
}
.biometrics-enroll__body { display: flex; flex-direction: column; gap: 4px; }
.biometrics-enroll__name {
font-weight: var(--font-weight-semibold);
font-size: var(--text-sm);
color: var(--color-text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.biometrics-enroll__labels {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.biometrics-enroll__labels li {
font-size: var(--text-xs);
color: var(--color-text-secondary);
background: var(--color-bg-secondary);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
.biometrics-enroll__labels li span {
color: var(--color-text-muted);
margin-right: 4px;
}
.biometrics-enroll__meta {
font-size: var(--text-xs);
color: var(--color-text-muted);
display: inline-flex;
align-items: center;
gap: 4px;
}
.biometrics-enroll__delete {
position: absolute;
top: 8px;
right: 8px;
background: var(--color-bg-overlay);
border: 1px solid var(--color-border-subtle);
color: var(--color-text-muted);
border-radius: var(--radius-sm);
width: 28px;
height: 28px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity var(--duration-fast), color var(--duration-fast), background var(--duration-fast);
}
.biometrics-enroll__card:hover .biometrics-enroll__delete,
.biometrics-enroll__card:focus-within .biometrics-enroll__delete { opacity: 1; }
.biometrics-enroll__delete:hover {
color: var(--color-error);
background: var(--color-error-light);
border-color: var(--color-error-border);
}
.biometrics-enroll__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
padding: var(--spacing-xl);
border: 1px dashed var(--color-border-default);
border-radius: var(--radius-lg);
text-align: center;
color: var(--color-text-secondary);
background: var(--color-bg-secondary);
}
.biometrics-enroll__empty > .lai-icon {
font-size: 2rem;
color: var(--color-accent);
opacity: 0.6;
}
.biometrics-enroll__empty p {
margin: 0;
max-width: 44ch;
line-height: var(--leading-normal);
font-size: var(--text-sm);
}
/* Matches list (identify results) */
.biometrics-matches {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.biometrics-matches__empty {
padding: var(--spacing-md);
border: 1px dashed var(--color-border-default);
border-radius: var(--radius-md);
color: var(--color-text-muted);
text-align: center;
font-size: var(--text-sm);
}
.biometrics-matches__row {
display: grid;
grid-template-columns: 32px 56px 1fr;
gap: var(--spacing-sm);
align-items: center;
padding: var(--spacing-sm);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
}
.biometrics-matches__row.match { border-color: var(--color-success-border); }
.biometrics-matches__rank {
font-size: var(--text-xs);
color: var(--color-text-muted);
font-weight: var(--font-weight-semibold);
text-align: center;
}
.biometrics-matches__avatar {
width: 56px;
height: 56px;
border-radius: var(--radius-md);
overflow: hidden;
background: var(--color-surface-sunken);
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
font-weight: var(--font-weight-semibold);
font-size: var(--text-sm);
}
.biometrics-matches__avatar img { width: 100%; height: 100%; object-fit: cover; }
.biometrics-matches__body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.biometrics-matches__name {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--text-sm);
min-width: 0;
}
.biometrics-matches__name strong {
font-weight: var(--font-weight-semibold);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.biometrics-matches__badge {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 2px 6px;
border-radius: var(--radius-sm);
background: var(--color-bg-tertiary);
color: var(--color-text-muted);
display: inline-flex;
align-items: center;
gap: 4px;
}
.biometrics-matches__badge.match {
background: var(--color-success-light);
color: var(--color-success);
}
.biometrics-matches__meter {
height: 4px;
background: var(--color-bg-tertiary);
border-radius: var(--radius-full);
overflow: hidden;
}
.biometrics-matches__fill {
height: 100%;
background: var(--color-accent);
transition: width var(--duration-normal) var(--ease-default);
}
.biometrics-matches__row.match .biometrics-matches__fill { background: var(--color-success); }
.biometrics-matches__meta {
display: flex;
gap: var(--spacing-md);
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.biometrics-matches__meta code {
color: var(--color-text-secondary);
font-variant-numeric: tabular-nums;
}
.biometrics-matches__preview { width: 100%; }
/* Embedding inspector */
.biometrics-embed {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
padding: var(--spacing-md);
}
.biometrics-embed__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--spacing-sm);
}
.biometrics-embed__title {
font-size: var(--text-base);
font-weight: var(--font-weight-semibold);
}
.biometrics-embed__meta {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-md);
font-size: var(--text-xs);
color: var(--color-text-muted);
margin-top: 4px;
}
.biometrics-embed__meta strong { color: var(--color-text-primary); font-variant-numeric: tabular-nums; font-weight: var(--font-weight-semibold); }
.biometrics-embed__meta code { color: var(--color-text-secondary); }
/* Response details pane */
.biometrics-response {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
}
.biometrics-response summary {
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
font-size: var(--text-sm);
color: var(--color-text-secondary);
display: flex;
align-items: center;
gap: var(--spacing-xs);
list-style: none;
user-select: none;
min-height: 40px;
}
.biometrics-response summary::-webkit-details-marker { display: none; }
.biometrics-response summary .lai-icon { transition: transform var(--duration-fast); }
.biometrics-response[open] summary .lai-icon { transform: rotate(90deg); }
.biometrics-response pre {
margin: 0;
padding: var(--spacing-md);
background: var(--color-surface-sunken);
font-size: var(--text-xs);
color: var(--color-text-secondary);
overflow-x: auto;
max-height: 360px;
line-height: var(--leading-snug);
}
.form-label__hint {
color: var(--color-text-muted);
font-weight: var(--font-weight-regular);
margin-left: 4px;
}
/* ResourceRow — unified expandable row anatomy used by the Manage page so
installed models and backends share the same visual grammar as the Install
gallery (icon, name, description, badges, expandable detail panel). */
.resource-row { transition: background var(--duration-fast) var(--ease-default); }
.resource-row.is-dimmed { opacity: 0.55; transition: opacity 0.2s; }
.resource-row.is-expanded { background: var(--color-bg-tertiary); }
.resource-row__chevron-cell { width: 30px; }
.resource-row__icon-cell { width: 64px; }
.resource-row__icon {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
border: 1px solid var(--color-border-subtle);
background: var(--color-bg-primary);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.resource-row__icon > img {
width: 100%;
height: 100%;
object-fit: cover;
}
.resource-row__icon > .lai-icon {
font-size: 1.25rem;
color: var(--color-accent);
}
.resource-row__detail-row > .resource-row__detail-cell {
padding: 0;
background: var(--color-bg-primary);
border-top: 1px solid var(--color-border-subtle);
}
.resource-row__detail {
padding: var(--spacing-md) var(--spacing-lg);
}
.resource-row__detail h4 {
font-size: var(--text-xs);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-text-muted);
margin: 0 0 var(--spacing-sm) 0;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.resource-row__detail h4 .lai-icon { color: var(--color-accent); }
.resource-row__detail-grid {
display: grid;
grid-template-columns: max-content 1fr;
gap: 6px var(--spacing-md);
}
.resource-row__detail-grid dt {
color: var(--color-text-muted);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
align-self: start;
padding-top: 2px;
}
.resource-row__detail-grid dd {
margin: 0;
font-size: var(--text-sm);
color: var(--color-text-primary);
min-width: 0;
word-break: break-word;
}
.resource-row__detail-md {
color: var(--color-text-secondary);
line-height: 1.6;
font-size: var(--text-sm);
}
.resource-row__detail-md p:first-child { margin-top: 0; }
.resource-row__detail-md p:last-child { margin-bottom: 0; }
/* Description line directly under the row name — Install gallery already
does this; Manage rows used to show only the bare name. */
.resource-row__desc {
display: block;
font-size: var(--text-xs);
color: var(--color-text-muted);
max-width: 42ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 2px;
}
/* ResourceActions — split lifecycle vs destructive with a thin divider so
the trash icon doesn't sit at the same eye-weight as the routine buttons.
Used now only for the rare row whose actions can't collapse into the
kebab menu (e.g. a "Protected" badge on system backends). */
.resource-actions {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
justify-content: flex-end;
}
.resource-actions__group {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
}
.resource-actions__divider {
width: 1px;
height: 20px;
background: var(--color-border-subtle);
margin: 0 var(--spacing-xs);
flex-shrink: 0;
}
/* ActionMenu — kebab trigger + popover menu. Borrows claudemaster's
restrained pattern: button reads as a quiet ellipsis at rest, lights up
on row hover, and the menu items hold typography-first weight (icon +
label, no fills until hover). The trigger stays at low opacity until the
user reaches for the row, so dense tables don't read as control panels. */
.action-menu__trigger {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: 1px solid transparent;
border-radius: var(--radius-md);
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
font-size: var(--text-sm);
opacity: 0.45;
transition:
opacity var(--dk-dur-1) ease,
color var(--dk-dur-1) ease,
background-color var(--dk-dur-1) ease,
border-color var(--dk-dur-1) ease,
transform var(--dk-dur-1) var(--dk-ease-out);
}
.action-menu__trigger:active { transform: scale(var(--dk-press-scale)); }
.resource-row:hover .action-menu__trigger,
.action-menu__trigger:focus-visible,
.action-menu__trigger.is-open {
opacity: 1;
}
.action-menu__trigger:hover,
.action-menu__trigger.is-open {
background: var(--color-bg-hover);
border-color: transparent;
color: var(--color-text-primary);
}
.action-menu__trigger:focus-visible {
outline: 2px solid var(--color-border-focus);
outline-offset: 2px;
}
.action-menu__trigger--compact {
width: 24px;
height: 24px;
font-size: var(--text-xs);
}
@media (hover: none) {
.action-menu__trigger { opacity: 1; }
}
.action-menu {
display: flex;
flex-direction: column;
min-width: 200px;
padding: 4px;
outline: none;
}
.action-menu__item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: 0 var(--spacing-sm);
min-height: 40px;
border: 0;
border-radius: var(--radius-md);
background: transparent;
color: var(--color-text-primary);
font-family: inherit;
font-size: var(--text-sm);
font-weight: 500;
text-align: left;
cursor: pointer;
width: 100%;
transition: background-color var(--dk-dur-1) ease,
color var(--dk-dur-1) ease,
transform var(--dk-dur-1) var(--dk-ease-out);
}
.action-menu__item.is-active,
.action-menu__item:hover:not(:disabled) {
background: var(--color-bg-hover);
}
.action-menu__item:active:not(:disabled) {
transform: scale(var(--dk-press-scale-nav));
}
.action-menu__item:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.action-menu__icon {
width: 16px;
height: 16px;
text-align: center;
color: var(--color-text-muted);
font-size: var(--text-xs);
flex-shrink: 0;
}
.action-menu__item.is-active .action-menu__icon,
.action-menu__item:hover:not(:disabled) .action-menu__icon {
color: var(--color-text-primary);
}
.action-menu__label {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.action-menu__shortcut {
font-family: var(--font-mono);
font-size: 10px;
color: var(--color-text-muted);
letter-spacing: 0.04em;
flex-shrink: 0;
}
.action-menu__item.is-danger { color: var(--dk-error-text); }
.action-menu__item.is-danger .action-menu__icon { color: var(--dk-error-text); }
.action-menu__item.is-danger:hover:not(:disabled),
.action-menu__item.is-danger.is-active {
background: var(--color-error-light);
color: var(--dk-error-text);
}
.action-menu__item.is-danger:hover:not(:disabled) .action-menu__icon,
.action-menu__item.is-danger.is-active .action-menu__icon {
color: var(--dk-error-text);
}
.action-menu__divider {
height: 1px;
background: var(--color-border-subtle);
margin: 4px 2px;
}
.action-menu__badge {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: 8px 10px;
font-size: var(--text-xs);
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-text-muted);
}
.action-menu__badge > .lai-icon { color: var(--color-text-muted); font-size: var(--text-xs); width: 14px; text-align: center; }
/* StatCard clickable variant — the Manage summary cards double as shortcuts
to the relevant tab + filter, so the card needs a real button affordance. */
.stat-card[data-clickable="true"] {
cursor: pointer;
user-select: none;
}
.stat-card[data-clickable="true"]:hover {
border-color: var(--stat-accent, var(--color-border));
transform: translateY(-1px);
}
.stat-card[data-clickable="true"]:focus-visible {
outline: 2px solid var(--color-border-focus);
outline-offset: 2px;
}
/* Screen-reader-only label, used for table headers whose visual cell needs
no label (kebab-only Actions column, toggle-only Enabled column). The
header still announces correctly to assistive tech without making the
table feel like it's labelling sparse columns twice. */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Reduced motion accessibility */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Lazy-route loading fallback. Delayed ~150ms so fast chunk loads don't flash. */
.route-fallback {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 40vh;
opacity: 0;
animation: route-fallback-in var(--duration-normal) var(--ease-out, ease-out) 150ms forwards;
}
@keyframes route-fallback-in { to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
.route-fallback { opacity: 1; animation: none; }
}
/* Hub layout: a page-level tab bar (the kit's dk-hubtabs) above the page body.
Build and Operate use it in place of a second navigation rail. */
.hub-layout {
display: flex;
flex-direction: column;
padding-top: var(--spacing-md);
}
/* The bar lines up with the page padding and caps at the widest page. */
.hub-bar {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
width: 100%;
max-width: var(--page-max-default);
margin: 0 auto;
padding: 0 var(--spacing-xl);
min-width: 0;
}
/* The bar scrolls sideways on its own; it never widens the page. */
.hub-bar .dk-hubtabs { align-self: flex-start; }
/* Second row for a tab that owns several routes: quiet text links. */
.hub-subnav {
display: flex;
gap: var(--spacing-xs);
max-width: 100%;
overflow-x: auto;
scrollbar-width: none;
}
.hub-subnav::-webkit-scrollbar { display: none; }
.hub-subnav__link {
flex: none;
display: inline-flex;
align-items: center;
height: 32px;
padding: 0 var(--spacing-sm);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
font-size: var(--text-sm);
font-weight: var(--font-weight-semibold);
text-decoration: none;
white-space: nowrap;
transition: background-color var(--duration-fast) ease, color var(--duration-fast) ease;
}
.hub-subnav__link:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
.hub-subnav__link[aria-current="page"] {
color: var(--color-accent-text, var(--dk-accent-text));
box-shadow: inset 0 -2px 0 var(--dk-accent);
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.hub-body {
flex: 1 1 auto;
min-width: 0;
width: 100%;
}
/* A medium page is centred in the content area, but the tab bar above it is
flush with the page edge, so the title sat 60px to the right of the first
tab. Inside a hub the medium column starts where the bar starts. */
.hub-body .page--medium {
margin-left: max(0px, calc((100% - var(--page-max-default)) / 2));
margin-right: auto;
}
@media (max-width: 768px) {
.hub-layout { padding-top: var(--spacing-sm); }
.hub-bar { padding: 0 var(--spacing-md); }
/* Let the bar run to the screen edge so a half-visible tab says "scroll". */
.hub-bar .dk-hubtabs { align-self: stretch; }
}
.status-pill {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.status-pill__dot { width: 7px; height: 7px; border-radius: var(--radius-full); background: var(--color-text-muted); flex-shrink: 0; }
.status-pill--success .status-pill__dot { background: var(--color-success); }
.status-pill--warning .status-pill__dot { background: var(--color-warning); }
.status-pill--error .status-pill__dot { background: var(--color-error); }
.status-pill--info .status-pill__dot { background: var(--color-info); }
.status-pill--muted .status-pill__dot { background: var(--color-text-muted); }
/* ──────────────────── Voice Library ──────────────────── */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.form-error { margin: var(--spacing-sm) 0 0; color: var(--color-error); font-size: var(--text-sm); }
.form-hint { margin: var(--spacing-xs) 0 0; color: var(--color-text-tertiary); font-size: var(--text-xs); line-height: var(--leading-normal); }
/* Nodes: cluster pulse + attention callout (replaces the stat-card strip) */
.attention-callout {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-md);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-lg);
font-size: var(--text-sm);
}
.attention-callout--error {
background: var(--color-error-light);
border: 1px solid var(--color-error-border);
color: var(--color-text-primary);
}
/* Node roster panels (Nodes page) */
.node-roster { display: flex; flex-direction: column; gap: var(--spacing-sm); }
.model-chip {
display: inline-flex; align-items: center; gap: 5px;
font-family: var(--font-mono); font-size: 0.6875rem;
padding: 2px 8px; border-radius: var(--radius-sm); border: 1px solid;
}
.model-chip__dot { width: 6px; height: 6px; border-radius: 50%; }
.model-chip__state { opacity: 0.85; font-style: normal; }
/* This machine: the single-node view of the Nodes page and the Operate
overview's "Running now" preview. */
.host-memory__track { fill: var(--color-bg-tertiary); }
.host-memory__segment--0, .host-memory__dot--0 { fill: var(--color-primary); background: var(--color-primary); }
.host-memory__segment--1, .host-memory__dot--1 { fill: var(--color-success); background: var(--color-success); }
.host-memory__segment--2, .host-memory__dot--2 { fill: var(--color-info); background: var(--color-info); }
.host-memory__segment--3, .host-memory__dot--3 { fill: var(--color-accent); background: var(--color-accent); }
.host-memory__segment--4, .host-memory__dot--4 { fill: var(--color-secondary); background: var(--color-secondary); }
.host-memory__segment + .host-memory__segment { stroke: var(--color-bg-secondary); stroke-width: .4; }
.host-memory__name { overflow: hidden; text-overflow: ellipsis; }
.local-running { margin-bottom: var(--spacing-lg); }
.local-running__empty a { color: var(--color-primary); }
.local-running__more { border-top: 1px solid var(--color-border-subtle); }
.local-model-table th:first-child, .local-model-table td:first-child { width: 30%; }
@keyframes node-inspector-scrim-in { from { opacity: 0; } to { opacity: 1; } }
/* Rendered Markdown ---------------------------------------------------------
Gallery descriptions, backend descriptions and voice notes are all
author-supplied Markdown pasted from model cards. Left unscoped they inherit
browser defaults, so a description that opens with `#` renders a 2em heading
inside a 13px surface while a `##` further down is indistinguishable from
body text. This maps the whole element set onto the UI type scale so any
description reads as part of the app rather than as a document dropped into
it. Apply it to every element fed by renderMarkdown(). */
.markdown-body {
color: var(--color-text-secondary);
font-size: var(--text-sm);
line-height: var(--leading-normal);
}
.markdown-body > :first-child { margin-top: 0; }
.markdown-body > :last-child { margin-bottom: 0; }
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
color: var(--color-text-primary);
font-weight: var(--font-weight-semibold);
line-height: var(--leading-tight);
margin: var(--spacing-md) 0 var(--spacing-xs);
}
.markdown-body h1 { font-size: var(--text-lg); }
.markdown-body h2 { font-size: var(--text-base); }
.markdown-body h3 { font-size: var(--text-sm); }
/* Below h3 the scale has no room left to separate levels by size, so the
remaining depth is carried by case and weight instead. */
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
font-size: var(--text-xs);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.markdown-body p { margin: 0 0 var(--spacing-sm); }
.markdown-body ul,
.markdown-body ol {
margin: 0 0 var(--spacing-sm);
padding-left: var(--spacing-lg);
}
.markdown-body li { margin-bottom: var(--spacing-xs); }
.markdown-body li:last-child { margin-bottom: 0; }
.markdown-body li > ul,
.markdown-body li > ol { margin-top: var(--spacing-xs); }
.markdown-body a {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: 2px;
}
.markdown-body a:hover { color: var(--color-primary-hover); }
.markdown-body code {
font-family: var(--font-mono);
font-size: 0.9em;
background: var(--color-bg-tertiary);
border-radius: var(--radius-sm);
padding: 1px 4px;
}
.markdown-body pre {
margin: 0 0 var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow-x: auto;
}
.markdown-body pre code {
background: none;
padding: 0;
font-size: var(--text-xs);
}
.markdown-body blockquote {
margin: 0 0 var(--spacing-sm);
padding-left: var(--spacing-sm);
font-style: italic;
color: var(--color-text-muted);
}
.markdown-body img {
max-width: 100%;
height: auto;
border-radius: var(--radius-sm);
}
.markdown-body hr {
border: 0;
border-top: 1px solid var(--color-border-divider);
margin: var(--spacing-md) 0;
}
/* Tables are the one element that can exceed the measure, so they scroll in
their own box rather than widening the surface around them. */
.markdown-body table {
display: block;
width: max-content;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
margin: 0 0 var(--spacing-sm);
font-size: var(--text-xs);
}
.markdown-body th,
.markdown-body td {
border: 1px solid var(--color-border-subtle);
padding: var(--spacing-xs) var(--spacing-sm);
text-align: left;
}
.markdown-body th {
background: var(--color-bg-tertiary);
font-weight: var(--font-weight-medium);
}
/* Prose block in a detail pane. The label/value table beside it is right for
scalars, but multi-paragraph prose in a value cell runs the full pane width
and swamps the rows it sits above, so the description gets its own
full-width block with a reading measure. */
.detail-prose {
margin-bottom: var(--spacing-md);
}
.detail-prose__label {
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-xs);
}
.detail-prose__body {
max-width: 68ch;
}
/* Variant list --------------------------------------------------------------
One row per alternative build of the same model. Names vary in length, so
the rows share the parent's tracks and line up as columns; the name leads
because it is what the reader is choosing between. Only the informative
status is badged: "fits" is true of nearly every row and says nothing, while
a build that does not fit, and the one that plain Install would pick, do. */
.variant-list {
display: grid;
width: 100%;
/* info, name, backend, quantization, size, status, action, filler.
Every informative track is content-sized and the filler takes the slack,
so the rows stay packed together rather than stranding the install
affordance an inch of empty space away from the name it acts on. The
grid still spans the pane, which is what lets a revealed detail panel
have the full width its file table needs. */
grid-template-columns: max-content minmax(0, auto) max-content max-content max-content max-content max-content 1fr;
column-gap: var(--spacing-md);
row-gap: 2px;
}
/* One variant: the info control, the install row, and the details the control
reveals. It spans the list's tracks and passes them down, so the install row
still shares its columns with every other row despite the extra nesting. */
.variant-entry {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
align-items: center;
}
.variant-row {
display: grid;
/* Stops short of the filler track so the hover and focus box hugs the row's
own content rather than trailing across the empty right-hand side. */
grid-column: 2 / -2;
grid-template-columns: subgrid;
align-items: center;
width: 100%;
margin: 0;
padding: var(--spacing-xs) var(--spacing-sm);
font: inherit;
text-align: left;
color: inherit;
background: none;
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background-color var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default);
}
/* Subgrid keeps the columns shared across rows; without it each row falls back
to sizing its own tracks, which is ragged but still legible. */
@supports not (grid-template-columns: subgrid) {
.variant-entry { grid-template-columns: max-content minmax(22ch, auto) max-content max-content max-content max-content max-content 1fr; }
.variant-row { grid-template-columns: minmax(22ch, auto) max-content max-content max-content max-content max-content; }
}
.variant-row:hover:not(:disabled) {
background: var(--color-bg-hover);
border-color: var(--color-border-default);
}
.variant-row:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 1px;
}
.variant-row:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.variant-row__name {
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
overflow: hidden;
text-overflow: ellipsis;
}
.variant-row__backend,
.variant-row__size {
font-size: var(--text-xs);
color: var(--color-text-muted);
}
/* Monospaced like the name above it, because a weight format is a literal
token and Q4_K_M vs Q4_K_S has to be told apart at a glance in a column. */
.variant-row__quant {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-text-secondary);
white-space: nowrap;
}
/* An entry naming no weight format is a real state, not a gap: it recedes to
the muted colour and drops the mono face, so it reads as prose saying
"unknown" rather than as a token. */
.variant-row__quant--unknown {
font-family: inherit;
font-style: italic;
color: var(--color-text-disabled);
}
.variant-row__size {
text-align: right;
font-variant-numeric: tabular-nums;
}
.variant-row__status {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
flex-wrap: wrap;
}
.variant-row__action {
color: var(--color-text-disabled);
font-size: var(--text-xs);
transition: color var(--duration-fast) var(--ease-default);
}
.variant-row:hover:not(:disabled) .variant-row__action,
.variant-row:focus-visible .variant-row__action {
color: var(--color-primary);
}
/* A build that does not fit stays installable, an explicit choice being an
override the server honours, but it should not read as a peer of the ones
that do. */
.variant-row--unfit .variant-row__name {
color: var(--color-text-muted);
}
/* Quiet until asked for: the row's job is to be compared and installed, so the
way into "everything about this one" recedes until the eye or the keyboard
lands on it. */
.variant-row__info {
grid-column: 1;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
margin: 0;
padding: 0;
font-size: var(--text-xs);
color: var(--color-text-disabled);
background: none;
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
transition: color var(--duration-fast) var(--ease-default),
background-color var(--duration-fast) var(--ease-default);
}
.variant-row__info:hover,
.variant-entry:hover .variant-row__info {
color: var(--color-primary);
}
.variant-row__info:hover {
background: var(--color-bg-hover);
}
.variant-row__info:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 1px;
color: var(--color-primary);
}
.variant-row__info[aria-expanded="true"] {
color: var(--color-primary);
background: var(--color-bg-hover);
border-color: var(--color-border-default);
}
/* The third level of disclosure on this page, so it leans on an inset and a
rule rather than another card: a nested panel with its own border inside an
expanded row inside an expanded table reads as debris. */
.variant-detail {
grid-column: 1 / -1;
min-width: 0;
margin: var(--spacing-xs) 0 var(--spacing-sm) var(--spacing-lg);
border: 1px solid var(--color-border-default);
background: var(--color-surface-sunken);
border-radius: var(--radius-md);
overflow: hidden;
}
.variant-detail__state {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.variant-detail__state--error {
color: var(--color-error);
}
@media (prefers-reduced-motion: reduce) {
.variant-row,
.variant-row__info,
.variant-row__action { transition: none; }
}
.nav-badge {
min-width: 18px;
height: 18px;
padding: 0 var(--spacing-xs);
border-radius: var(--radius-full);
background: var(--color-primary-light);
color: var(--dk-accent-text);
font-family: var(--font-mono);
font-size: 0.625rem;
font-weight: 700;
line-height: 18px;
text-align: center;
font-variant-numeric: tabular-nums;
}
.nav-badge--error { background: var(--color-error-light); color: var(--dk-error-text); }
/* Collapsed, the label is gone and the item centres its icon: an inline badge
would shove that icon off the rail's axis. Pinning it to the corner keeps the
count visible without moving anything else. */
.sidebar.collapsed .nav-badge {
position: absolute;
top: 2px;
right: 4px;
min-width: 16px;
height: 16px;
line-height: 16px;
font-size: 0.5625rem;
}
/* Native file inputs carry their own button and filename text, so the standard
.input padding pushes "No file chosen" past the right edge and truncates it.
Also affects ImageGen and VideoGen, which hit this today. */
.input--file { padding: 6px; }
/* ===========================================================================
Layout and text primitives
These exist because ~1,700 inline styles across the pages were re-deriving
the same handful of intents: vertical rhythm, a row of related controls, a
line of secondary text. Each of those is a decision that should be made once
for the whole app, not per JSX node. Keep this list SHORT and semantic; it
is deliberately not a utility framework. If a page needs a shape that is not
here, give that shape a name in its own block rather than growing this one.
=========================================================================== */
/* Vertical rhythm. Replaces chains of marginBottom on siblings, which collapse
and double in ways that are invisible until two pages sit side by side. */
.stack { display: flex; flex-direction: column; gap: var(--spacing-md); }
.stack--sm { gap: var(--spacing-sm); }
.stack--lg { gap: var(--spacing-lg); }
/* A row of related things: label + control, button groups, icon + text. Wraps
by default because every one of these eventually meets a narrow viewport. */
.hstack {
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex-wrap: wrap;
min-width: 0;
}
.hstack--md { gap: var(--spacing-md); }
.hstack--nowrap { flex-wrap: nowrap; }
.hstack--between { justify-content: space-between; }
.hstack--end { justify-content: flex-end; }
.hstack--top { align-items: flex-start; }
/* Secondary and tertiary text. The pages had eleven different combinations of
font-size and muted colour for what is the same two roles. */
.text-note { font-size: var(--text-sm); color: var(--color-text-muted); }
.text-meta { font-size: var(--text-xs); color: var(--color-text-muted); }
.text-mono { font-family: var(--font-mono); }
/* The "waiting for the first response" wrapper, previously repeated inline in
25 places with identical values. */
.loading-center {
display: flex;
justify-content: center;
padding: var(--spacing-xl);
}
/* Grow-to-fill inside a .hstack. Named rather than inline because "which child
absorbs the slack" is a layout decision worth being able to grep for. */
.flex-1 { flex: 1; }
/* Same, plus the min-width:0 that lets a flex child actually shrink and
ellipsis instead of overflowing its row. */
.flex-1-min { flex: 1; min-width: 0; }
/* Semantic tones. Sets three custom properties so a component can be tinted by
meaning without every call site naming colours. Pair with .icon-chip, or any
block that reads --tone / --tone-soft / --tone-line. */
.tone-primary { --tone: var(--color-primary); --tone-soft: var(--color-primary-light); --tone-line: var(--color-primary-border); }
.tone-accent { --tone: var(--color-accent); --tone-soft: var(--color-accent-light); --tone-line: var(--color-accent-border); }
.tone-success { --tone: var(--color-success); --tone-soft: var(--color-success-light); --tone-line: var(--color-success-border); }
.tone-warning { --tone: var(--color-warning); --tone-soft: var(--color-warning-light); --tone-line: var(--color-warning-border); }
.tone-error { --tone: var(--color-error); --tone-soft: var(--color-error-light); --tone-line: var(--color-error-border); }
.tone-muted { --tone: var(--color-text-muted); --tone-soft: var(--color-bg-tertiary); --tone-line: var(--color-border-subtle); }
/* The tinted rounded square holding a single icon. Appears on P2P, Home,
Manage and Nodes, previously rebuilt inline each time with its own size. */
.icon-chip {
width: 40px; height: 40px;
border-radius: var(--radius-md);
display: grid; place-items: center;
flex: none;
background: var(--tone-soft, var(--color-bg-tertiary));
color: var(--tone, var(--color-text-muted));
font-size: 1rem;
}
.icon-chip--sm { width: 36px; height: 36px; font-size: 0.75rem; }
.icon-chip--lg { width: 48px; height: 48px; }
.icon-chip--ring { border: 2px solid var(--tone); }
/* ===========================================================================
P2P / distributed
The page explains three architectures (federation, llama.cpp RPC sharding,
MLX pipeline) using the same four shapes each time: a diagram, a live count,
a node grid and a setup card. Those shapes are named once here.
=========================================================================== */
/* Token */
.p2p-token {
background: var(--color-bg-secondary);
border: 1px solid var(--color-accent-border);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
/* Tabs */
.p2p-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background: transparent;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
border-radius: var(--radius-md) var(--radius-md) 0 0;
font: inherit;
cursor: pointer;
color: var(--color-text-secondary);
transition: background var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default);
}
/* Architecture diagrams */
/* Live counts */
/* Node grid + card */
/* Repeated "nothing connected yet" box */
/* Copyable command */
/* .btn alone leaves the background to the user agent, which paints
`buttonface` — a light grey. Fine on a light theme, a pale chip on a dark
command block. State it. */
/* Numbered setup steps */
/* Callout distinguishing federation from sharding */
/* Disabled state */
.p2p-enable {
max-width: 700px;
margin: 0 auto var(--spacing-xl);
text-align: left;
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
/* Centre a row of controls, e.g. a pair of documentation buttons. */
.hstack--center { justify-content: center; }
/* An icon that takes the colour of its nearest .tone-* ancestor. Section
headings across the app pair an icon with a semantic colour; this stops each
one naming the colour again. */
.tone-icon { color: var(--tone, var(--color-text-muted)); }
/* Occasional extra separation between two steps inside a setup card, where the
normal .stack gap would under-sell the break. */
.push-lg { margin-top: var(--spacing-lg); }
/* Secondary text at the sm step. Distinct from .text-note: --color-text-secondary
is for content, --color-text-muted is for metadata about content. */
.text-sub { font-size: var(--text-sm); color: var(--color-text-secondary); }
/* Colour only, when the size is already right. */
.text-muted { color: var(--color-text-muted); }
/* Type scale steps. Named so that a size which is NOT on the scale stays an
inline style and therefore stays visible: 0.75rem was used ~100 times and
sits between --text-xs and --text-sm. */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-base { font-size: var(--text-base); }
/* Semantic text colours. */
.text-primary { color: var(--color-primary); }
.text-secondary { color: var(--color-text-secondary); }
.text-error { color: var(--color-error); }
/* An icon sitting immediately before a label, where the parent is not a flex
row with a gap. */
.icon-before { margin-right: var(--spacing-xs); }
.text-right { text-align: right; }
.clickable { cursor: pointer; }
.m-0 { margin: 0; }
.mb-0 { margin-bottom: 0; }
/* Bold title inside a card or panel, with its icon inline. Thirteen pages
rebuilt this same heading with the same six declarations. */
.panel-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--text-lg);
font-weight: var(--font-weight-bold, 700);
margin: 0 0 var(--spacing-md);
}
/* A row in a plain divided list (settings, details, key/value readouts). */
.list-row {
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--color-border-subtle);
}
.list-row:last-child { border-bottom: 0; }
/* Spacing and weight steps, locked to the scale.
PREFER .stack on the parent over margins on siblings: it cannot collapse or
double. These exist for the one-off cases where a single element needs to be
pushed off its neighbour, and their real value is that they make an
off-scale value (marginBottom: 13) stay visible as an inline style. */
.mb-xs { margin-bottom: var(--spacing-xs); }
.mb-sm { margin-bottom: var(--spacing-sm); }
.mb-md { margin-bottom: var(--spacing-md); }
.mb-lg { margin-bottom: var(--spacing-lg); }
.mb-xl { margin-bottom: var(--spacing-xl); }
.mt-xs { margin-top: var(--spacing-xs); }
.mt-sm { margin-top: var(--spacing-sm); }
.mt-md { margin-top: var(--spacing-md); }
.pad-sm { padding: var(--spacing-sm); }
.pad-md { padding: var(--spacing-md); }
.pad-lg { padding: var(--spacing-lg); }
.fw-medium { font-weight: var(--font-weight-medium); }
.fw-semibold { font-weight: var(--font-weight-semibold); }
/* Centred "nothing to show" text inside an already-bounded container, where the
full .empty-state would be too heavy. */
.inline-empty {
text-align: center;
color: var(--color-text-muted);
padding: var(--spacing-md);
}
/* Remaining shared shapes found while converting the pages. */
.hstack--xs { gap: var(--spacing-xs); }
.stack--xs { gap: var(--spacing-xs); }
.mt-lg { margin-top: var(--spacing-lg); }
.text-accent { color: var(--color-accent); }
.text-center { text-align: center; }
.hidden { display: none; }
.p-0 { padding: 0; }
.w-full { width: 100%; }
/* A heading immediately above a group it labels. */
.group-label { font-weight: var(--font-weight-semibold); margin-bottom: var(--spacing-md); }
.group-label--tight { margin-bottom: var(--spacing-sm); }
.text-lg { font-size: var(--text-lg); }
.text-success { color: var(--color-success); }
.text-warning { color: var(--color-warning); }
/* Table column widths. Per-table CSS is the better home for these, but pulling
them out of the JSX first makes the ladder visible: there are currently
thirteen distinct column widths across the app where three or four would do.
Normalising them is a follow-up; this pass only stops them being inline. */
.col-w-30 { width: 30px; }
.col-w-40 { width: 40px; }
.col-w-64 { width: 64px; }
.col-w-80 { width: 80px; }
.col-w-90 { width: 90px; }
.col-w-100 { width: 100px; }
.col-w-110 { width: 110px; }
.col-w-120 { width: 120px; }
.col-w-130 { width: 130px; }
.col-w-140 { width: 140px; }
.col-w-170 { width: 170px; }
.col-w-200 { width: 200px; }
.col-w-220 { width: 220px; }
.col-w-240 { width: 240px; }
.col-w-320 { width: 320px; }
/* Small uppercase pill used for statuses and counts inside dense tables. */
.pill-xs {
font-size: var(--text-xs);
padding: 2px 8px;
}
/* Uppercase micro-label above a value or a chart. */
.overline {
font-size: var(--text-xs);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.ml-xs { margin-left: var(--spacing-xs); }
.d-block { display: block; }
.icon-xl { font-size: 2rem; }
/* Neutral chip for a value with no semantic state of its own. */
.chip-neutral { background: var(--color-bg-tertiary); color: var(--color-text-muted); }
.self-start { align-self: flex-start; }
.wrap-anywhere { white-space: pre-wrap; word-break: break-word; }
.min-h-control { min-height: 32px; }
.gap-xs { gap: var(--spacing-xs); }
.toolbar-divider { width: 1px; height: 20px; background: var(--color-border-subtle); margin: 0 var(--spacing-xs); }
.tone-data-3 { --tone: var(--color-data-3); }
.text-data-3 { color: var(--color-data-3); }
.text-italic { font-style: italic; }
.cursor-default { cursor: default; }
.fw-normal { font-weight: 400; }
.bg-secondary { background: var(--color-bg-secondary); }
.bg-muted { background: var(--color-bg-muted, var(--color-bg-secondary)); }
.w-160 { width: 160px; }
.min-w-100 { min-width: 100px; }
.mt-0 { margin-top: 0; }
.w-12 { width: 12px; }
.cell-clip { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cell-clip--wide { max-width: 300px; }
.nowrap { white-space: nowrap; }
.shrink-0 { flex-shrink: 0; }
.op-60 { opacity: 0.6; }
.op-70 { opacity: 0.7; }
.pad-xs { padding: var(--spacing-xs); }
.text-left { text-align: left; }
.hstack--sm { display: flex; align-items: center; gap: var(--spacing-sm); }
.badge--soft { background: var(--color-bg-tertiary); color: var(--color-text-secondary); }
.badge--tiny { font-size: 0.625rem; }
.badge--outline { background: var(--color-surface-sunken); color: var(--color-text-muted); border: 1px solid var(--color-border-subtle); }
.icon-tiny { font-size: 0.5rem; margin-right: 2px; }
.pagination-row { display: flex; align-items: center; justify-content: center; gap: var(--spacing-sm); }
.inline-flex { display: inline-flex; }
.search-grow { flex: 1; min-width: 200px; }
.badge--warn-soft { background: var(--color-warning-light); color: var(--color-warning); }
/* The .p2p-* setup shapes above (hero, feature chips, numbered steps, command
block, setup card) are shared with the Nodes page, which presents the same
"you have no workers yet, here is how to add one" flow. Renaming them to a
neutral prefix is a worthwhile follow-up; reusing them is what stops the
shapes diverging again in the meantime. */
.nodes-features { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-md); }
@media (max-width: 720px) { .nodes-features { grid-template-columns: minmax(0, 1fr); } }
/* Centre an icon chip above its label in a feature grid. */
.icon-chip--centred { margin: 0 auto var(--spacing-sm); }
.ml-auto { margin-left: auto; }
/* ===========================================================================
Node detail
The state pill (colours are data-driven), the small inline tag next to a
model name, and the section headers above each table.
=========================================================================== */
.state-pill {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
}
.state-pill--system { background: var(--color-bg-tertiary); color: var(--color-text-muted); border: 1px solid var(--color-border-subtle); }
.state-pill--gallery { background: var(--color-primary-light); color: var(--color-primary); border: 1px solid var(--color-primary-border); }
.inline-tag {
margin-left: var(--spacing-sm);
padding: 1px 6px;
border-radius: var(--radius-sm);
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border-subtle);
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
}
.table--flush { margin: 0; }
.link-plain { cursor: pointer; color: var(--color-primary); }
/* ===========================================================================
Config field renderer
Repeated across every model-config form: key/value list rows, the marker
tags next to a field label, and the compact numeric/text controls.
=========================================================================== */
.cfr-list { display: flex; flex-direction: column; gap: var(--spacing-xs); width: 100%; }
.cfr-row { display: flex; gap: var(--spacing-xs); align-items: center; }
.cfr-input { flex: 1; font-size: var(--text-sm); }
.cfr-tag { font-size: var(--text-xs); padding: 1px 4px; border-radius: var(--radius-sm); }
.cfr-tag--vram { background: var(--color-warning-light); color: var(--color-warning); }
.cfr-tag--advanced { background: var(--color-bg-tertiary); color: var(--color-text-muted); }
.cfr-clear {
background: none;
border: none;
cursor: pointer;
padding: 2px 4px;
color: var(--color-text-muted);
font-size: var(--text-xs);
font: inherit;
}
.cfr-textarea { width: 100%; min-height: 80px; font-size: var(--text-sm); resize: vertical; }
.cfr-textarea--mono { font-family: var(--font-mono); }
.cfr-slider-value { font-size: var(--text-sm); min-width: 40px; text-align: right; font-variant-numeric: tabular-nums; }
.cfr-num { width: 120px; font-size: var(--text-sm); }
.cfr-select { width: 220px; font-size: var(--text-sm); }
.cfr-radio { display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--text-sm); cursor: pointer; }
/* Failover targets editor — chain member list with a warm toggle and
duplicate/self-reference warnings. */
.fte-list { display: flex; flex-direction: column; gap: var(--spacing-sm); width: 100%; }
.fte-empty { font-size: var(--text-sm); color: var(--color-text-muted); padding: var(--spacing-sm) 0; }
.fte-warning { font-size: var(--text-xs); color: var(--color-warning); padding: var(--spacing-sm) 0; }
.fte-row { padding: var(--spacing-sm); display: flex; flex-direction: column; gap: var(--spacing-xs); }
.fte-row--error { border-color: var(--color-error); }
.fte-row__head { font-size: var(--text-xs); color: var(--color-text-muted); }
.pill-tiny { padding: 2px 6px; font-size: var(--text-xs); }
/* ===========================================================================
Section rail
The sticky rail the Model editor uses. The Settings page has its own
styles in pages/settings.css.
=========================================================================== */
.set-layout { display: flex; gap: 0; min-height: calc(100vh - 180px); }
.set-rail {
width: 200px;
flex-shrink: 0;
padding: 0 var(--spacing-sm);
position: sticky;
top: 0;
align-self: flex-start;
}
.set-rail__item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
width: 100%;
min-height: var(--control-h);
padding: 0 12px;
background: transparent;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
color: var(--color-text-secondary);
font: inherit;
font-size: var(--text-base);
text-align: left;
margin-bottom: 2px;
transition: background var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default);
}
.set-rail__item--on {
background: var(--color-primary-light);
color: var(--color-primary);
font-weight: var(--font-weight-semibold);
}
.set-rail__icon { width: 18px; text-align: center; font-size: var(--text-sm); color: var(--color-text-muted); }
.set-rail__item--on .set-rail__icon { color: var(--color-primary); }
/* One accent for the active section; a hue per section only decorated. */
.me-section-icon { color: var(--color-text-muted); }
/* On a phone the section rail cannot sit beside the form: the form rows were
squeezed into a few characters. Stack the rail above the content as a
scrolling row of sections, and let a form row wrap its control below. */
@media (max-width: 720px) {
.set-layout { flex-direction: column; }
.set-rail {
width: 100%;
position: static;
display: flex;
gap: var(--spacing-xs);
overflow-x: auto;
padding-bottom: var(--spacing-sm);
}
.set-rail__item { width: auto; flex: none; white-space: nowrap; margin-bottom: 0; }
}
/* ===========================================================================
Model editor
Reuses .set-rail* from Settings, which has the identical sticky section rail.
These are the shapes unique to this page: the tab strip, the blocked-field
warning strip and the collapsible section headers.
=========================================================================== */
.me-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-lg) var(--spacing-lg) var(--spacing-md);
}
/* Failover chain health strip (Model Editor, under the header) */
.failover-status {
padding: 0 var(--spacing-lg) var(--spacing-md);
border-bottom: 1px solid var(--color-border);
}
.failover-status__summary {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-xs) var(--spacing-md);
padding-bottom: var(--spacing-sm);
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.failover-status__label {
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--color-text-muted);
}
.failover-status__active { color: var(--color-text-primary); font-family: var(--font-mono); }
.failover-status__pinned { color: var(--color-warning); }
.failover-status__unpin { margin-left: auto; }
.failover-status__table td, .failover-status__table th { white-space: nowrap; }
.failover-status__table code { font-family: var(--font-mono); font-size: 0.8125rem; }
.failover-status__row--active td { background: var(--color-success-light); }
.failover-status__error {
display: inline-block;
max-width: 28ch;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
color: var(--color-error);
}
.failover-status__action { text-align: right; }
/* Failover overview (Operate -> Runtime): one row per chain, with a compact
strip of per-target pills standing in for the detailed table the Model
Editor shows for a single chain. */
.failover-overview__targets { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.failover-overview__target-pill { font-size: 0.75rem; }
.me-tabs { display: flex; gap: 0; padding: 0 var(--spacing-lg); border-bottom: 1px solid var(--color-border); }
.me-tab {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
background: transparent;
font: inherit;
font-size: var(--text-base);
color: var(--color-text-secondary);
}
.me-tab--on { font-weight: var(--font-weight-semibold); color: var(--color-primary); border-bottom-color: var(--color-primary); }
.me-tab--blocked { cursor: not-allowed; opacity: 0.5; }
.me-warn {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-lg);
font-size: var(--text-sm);
color: var(--color-warning);
background: var(--color-warning-light);
}
.me-body { flex: 1; padding: 0 var(--spacing-lg) var(--spacing-xl) var(--spacing-md); }
.me-section-head {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold, 700);
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.me-section-head--collapsed { margin-bottom: 0; }
.me-chevron { font-size: var(--text-xs); width: 12px; color: var(--color-text-muted); }
.me-pad { padding: 0 var(--spacing-lg); }
.me-pad--bottom { padding: 0 var(--spacing-lg) var(--spacing-lg); }
.max-w-400 { max-width: 400px; }
/* ===========================================================================
Node install picker
Modal chrome (header / body / footer bars), the warning cards, and the
label chips on each node row.
=========================================================================== */
.nip-bar {
padding: var(--spacing-md) var(--spacing-lg);
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
.nip-bar--head { border-bottom: 1px solid var(--color-border-subtle); justify-content: space-between; }
.nip-bar--foot { border-top: 1px solid var(--color-border-subtle); }
.nip-body { padding: var(--spacing-md) var(--spacing-lg); }
.nip-warn {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-warning-light);
border: 1px solid var(--color-warning-border);
border-radius: var(--radius-md);
display: flex;
}
.nip-warn--block { display: block; padding: var(--spacing-md); }
.nip-labels { display: flex; flex-wrap: wrap; gap: 3px; }
.nip-label {
padding: 1px 5px;
border-radius: var(--radius-sm);
font-size: var(--text-xs);
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border-subtle);
}
.nip-scroll { max-height: 40vh; overflow-y: auto; }
.btn--warning { background: var(--color-warning); border-color: var(--color-warning); color: var(--dk-on-warn); }
.badge-success--soft { background: var(--color-success-light); color: var(--color-success); }
.sr-offscreen { position: absolute; left: -9999px; }
.btn-help { border: none; cursor: help; }
.w-28 { width: 28px; }
.min-w-180 { min-width: 180px; }
/* ==========================================================================
SplitView: the shell shared by Models and Backends.
The rail is scanned, the pane answers. The pane has exactly two states -
a zero state with nothing selected and one entity's detail with something
selected - which is what replaces the click-to-expand table row. What the
zero state says is the surface's business: a catalog offers what to install,
an inventory reports what is running.
========================================================================== */
.split-view {
display: grid;
grid-template-columns: minmax(300px, 360px) minmax(0, 1fr);
gap: var(--spacing-md);
align-items: start;
}
.split-view__rail-col {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
min-width: 0;
}
.entity-rail {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.entity-rail__head {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--color-border-card);
background: var(--color-surface-sunken);
flex-wrap: wrap;
}
.entity-rail__count {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.entity-rail__sort {
display: flex;
gap: 2px;
margin-left: auto;
}
/* Sorting lost its home when the column headers went. It reappears here rather
than in the filter band above, because it orders this list and nothing else. */
.entity-rail__sort-btn {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--text-xs);
font-weight: var(--font-weight-semibold);
color: var(--color-text-muted);
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
padding: 4px var(--spacing-sm);
cursor: pointer;
}
.entity-rail__sort-btn:hover {
color: var(--color-text-primary);
border-color: var(--color-border-default);
}
.entity-rail__sort-btn.active {
color: var(--color-primary);
border-color: var(--color-primary-border);
background: var(--color-primary-light);
}
.entity-rail__list {
max-height: 640px;
overflow-y: auto;
}
.entity-rail__list:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
}
.entity-rail__group-head {
display: flex;
align-items: center;
gap: var(--spacing-xs);
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-surface-sunken);
border: 0;
border-top: 1px solid var(--color-border-card);
color: var(--color-text-secondary);
cursor: pointer;
text-align: left;
position: sticky;
top: 0;
z-index: 1;
}
.entity-rail__group:first-child .entity-rail__group-head { border-top: 0; }
.entity-rail__caret { font-size: 0.625rem; color: var(--color-text-muted); width: 12px; }
.entity-rail__group-icon { font-size: var(--text-xs); color: var(--color-text-muted); }
.entity-rail__group-label {
font-size: var(--text-xs);
font-weight: var(--font-weight-bold);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.entity-rail__group-count {
margin-left: auto;
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
}
.entity-rail__item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
width: calc(100% - var(--spacing-md));
margin: 2px var(--spacing-sm);
padding: 10px var(--spacing-sm);
background: transparent;
border: 0;
border-radius: var(--radius-md);
color: inherit;
text-align: left;
cursor: pointer;
}
.entity-rail__item:hover { background: var(--color-bg-hover); }
/* The rail rounds its corners with overflow:hidden, which clips an outline
drawn outside the element. Inset it so the first and last entries keep a
visible focus ring. */
.entity-rail__item:focus-visible,
.entity-rail__group-head:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
}
/* Touch. A 30px row is fine for a mouse and too small for a thumb, so coarse
pointers get the 44px target without costing density on a desktop. */
@media (pointer: coarse) {
.entity-rail__item { padding-top: 12px; padding-bottom: 12px; }
.entity-rail__group-head { padding-top: 10px; padding-bottom: 10px; }
}
/* The selected row: an accent wash with a hairline outline, in both themes. */
.entity-rail__item--on,
.entity-rail__item--on:hover {
background: var(--color-primary-light);
box-shadow: inset 0 0 0 1px var(--color-border-strong);
}
.entity-rail__icon {
flex: none;
width: 20px;
font-size: var(--text-base);
color: var(--color-text-muted);
text-align: center;
}
.entity-rail__item--on .entity-rail__icon { color: var(--dk-accent-text); }
.entity-rail__item--on .entity-rail__name { font-weight: var(--font-weight-bold); }
.entity-rail__main { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.entity-rail__name {
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.entity-rail__meta {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-text-tertiary);
font-variant-numeric: tabular-nums;
}
/* A small status dot for surfaces read by condition before they are read by
name. An inventory has one; a catalog does not, because "available" is not
a state worth a colour on every row. */
.entity-rail__stripe {
width: 8px;
height: 8px;
align-self: center;
border-radius: 50%;
flex: none;
}
.entity-rail__stripe--run { background: var(--color-success); }
.entity-rail__stripe--idle { background: var(--color-border-strong); }
.entity-rail__stripe--err { background: var(--color-error); }
.entity-rail__stripe--off { background: transparent; }
.entity-rail__meta--bad { color: var(--color-error); }
.entity-rail__meta--ok { color: var(--color-success); }
.entity-rail__meta--busy { color: var(--color-primary); }
.split-view__pager { margin: 0; }
.split-view__pager-label {
font-size: var(--text-sm);
color: var(--color-text-secondary);
padding: 0 var(--spacing-sm);
font-variant-numeric: tabular-nums;
}
/* --- Pane ---------------------------------------------------------------- */
.split-view__pane {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-sm);
min-height: 420px;
padding: var(--spacing-lg);
}
.zero-pane { display: flex; flex-direction: column; gap: var(--spacing-lg); }
.zero-pane__hero { display: flex; flex-direction: column; gap: 4px; }
.zero-pane__eyebrow {
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: var(--font-weight-bold);
letter-spacing: 0.11em;
text-transform: uppercase;
color: var(--color-eyebrow);
}
.zero-pane__title {
font-size: var(--text-xl);
font-weight: var(--font-weight-bold);
letter-spacing: -0.02em;
text-wrap: balance;
}
.zero-pane__text { font-size: var(--text-base); color: var(--color-text-muted); }
/* The heading needs air under it. Sitting a line's width from the text it
introduces made the two read as one paragraph. */
.zero-pane__shelf-head {
display: flex;
align-items: baseline;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
.zero-pane__shelf-title { font-size: var(--text-base); font-weight: var(--font-weight-bold); }
.zero-pane__shelf-meta {
margin-left: auto;
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.zero-pane__shelf-hint {
font-size: var(--text-sm);
color: var(--color-text-muted);
margin-top: 0;
margin-bottom: var(--spacing-sm);
}
/* Curated tiles. A catalog spends its zero state arguing for something; the
tile is where that argument gets the width a rail line never has. */
.zero-pane__tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: var(--spacing-sm);
}
.zero-pane__tile {
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-md);
padding: var(--spacing-sm);
display: flex;
flex-direction: column;
gap: 6px;
text-align: left;
cursor: pointer;
color: inherit;
}
.zero-pane__tile:hover { border-color: var(--color-border-strong); background: var(--color-bg-hover); }
.zero-pane__tile--feat {
border-color: var(--color-primary-border);
background: color-mix(in srgb, var(--color-primary-light) 45%, var(--color-bg-primary));
}
.zero-pane__tile-name { font-size: var(--text-sm); font-weight: var(--font-weight-semibold); }
.zero-pane__tile-foot { display: flex; align-items: center; gap: var(--spacing-xs); margin-top: auto; }
/* One line that changes what you would do next. Reserved for that; a pane of
these has no emphasis left. */
.zero-pane__alert {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--text-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
border: 1px solid;
}
.zero-pane__alert--warn { color: var(--color-warning); background: var(--color-warning-light); border-color: var(--color-warning-border); }
.zero-pane__alert--bad { color: var(--color-error); background: var(--color-error-light); border-color: var(--color-error-border); }
.zero-pane__alert > :last-child { margin-left: auto; }
.detail-pane__label {
display: block;
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: var(--font-weight-bold);
letter-spacing: 0.11em;
text-transform: uppercase;
color: var(--color-text-muted);
margin-bottom: 6px;
}
/* --- Detail -------------------------------------------------------------- */
.detail-pane { display: flex; flex-direction: column; gap: var(--spacing-md); }
.detail-pane__back {
align-self: flex-start;
display: inline-flex;
align-items: center;
gap: 6px;
min-height: var(--control-h-sm);
font-size: var(--text-xs);
color: var(--color-text-muted);
background: transparent;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
padding: 0 var(--spacing-sm);
cursor: pointer;
}
.detail-pane__back:hover { color: var(--color-text-primary); border-color: var(--color-border-strong); }
.detail-pane__head { display: flex; gap: var(--spacing-sm); align-items: flex-start; flex-wrap: wrap; }
.detail-pane__icon {
flex: none;
font-size: 1.25rem;
color: var(--dk-accent-text);
padding-top: 2px;
}
.detail-pane__title { flex: 1 1 260px; min-width: 0; }
.detail-pane__name {
font-family: var(--font-mono);
font-size: var(--text-xl);
font-weight: var(--font-weight-bold);
letter-spacing: -0.02em;
word-break: break-word;
}
.detail-pane__lede { font-size: var(--text-base); color: var(--color-text-muted); margin-top: 2px; }
.detail-pane__actions { display: flex; gap: var(--spacing-xs); align-items: center; flex: none; }
.detail-pane__warning {
display: flex;
align-items: center;
gap: 6px;
font-size: var(--text-sm);
color: var(--color-error);
background: var(--color-error-light);
border: 1px solid var(--color-error-border);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
}
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: var(--spacing-sm);
}
.stat-grid__item {
background: var(--color-surface-sunken);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
}
.stat-grid__item dt {
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: var(--font-weight-bold);
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-muted);
}
.stat-grid__item dd {
font-size: var(--text-xl);
font-weight: var(--font-weight-bold);
font-variant-numeric: tabular-nums;
margin-top: 1px;
}
.stat-grid__value--ok { color: var(--color-success); }
.stat-grid__value--bad { color: var(--color-error); }
.stat-grid__value--warn { color: var(--color-warning); }
/* --- VRAM by context ----------------------------------------------------- */
.discover__chart { display: flex; flex-direction: column; gap: 6px; }
.discover__chart-title {
font-family: var(--font-mono);
font-weight: var(--font-weight-bold);
font-size: var(--text-xs);
letter-spacing: 0.11em;
text-transform: uppercase;
color: var(--color-text-muted);
}
.discover__chart-plot {
/* Matches CHART_HEIGHT in Models.jsx: the bars and the limit line are both
resolved in pixels against this, so they share one scale. */
position: relative;
height: 96px;
display: flex;
align-items: flex-end;
gap: 8px;
padding-top: 15px;
}
/* The limit is what makes the bars mean anything, so it is drawn across all of
them rather than annotated on each. */
.discover__chart-limit {
position: absolute;
left: 0;
right: 0;
bottom: var(--discover-limit, 0);
/* Same tone as the bars that cross it: one colour, one meaning. */
border-top: 1px dashed var(--color-warning-border);
pointer-events: none;
}
.discover__chart-limit-label {
position: absolute;
right: 0;
top: -1.05rem;
font-family: var(--font-mono);
font-size: 0.5625rem;
letter-spacing: 0.05em;
color: var(--color-error);
background: var(--color-bg-secondary);
padding: 0 3px;
}
.discover__chart-col {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 3px;
height: 100%;
background: transparent;
border: 0;
padding: 0;
cursor: pointer;
min-width: 0;
}
.discover__chart-value {
font-family: var(--font-mono);
font-size: 0.5625rem;
color: var(--color-text-tertiary);
text-align: center;
font-variant-numeric: tabular-nums;
}
.discover__chart-bar {
height: var(--discover-bar, 0);
min-height: 3px;
border-radius: 4px 4px 0 0;
background: var(--color-success);
/* A surface-coloured ring keeps adjacent bars from reading as one block. */
box-shadow: 0 0 0 2px var(--color-bg-secondary);
}
/* Amber, not error red. An over-limit build still installs — #11288 keeps a
test on exactly that — so red would overstate it. Amber is the constraint
tone used everywhere else here: know what you are doing, not you may not. */
.discover__chart-bar--over { background: var(--color-warning); }
.discover__chart-col:hover .discover__chart-bar { filter: brightness(1.1); }
/* Its own row rather than a third line inside each column: one baseline for
every label, whatever the values above them do. */
.discover__chart-axis {
display: flex;
gap: 8px;
border-top: 1px solid var(--color-border-default);
padding-top: 3px;
}
.discover__chart-axis span {
flex: 1;
min-width: 0;
text-align: center;
font-family: var(--font-mono);
font-size: 0.5625rem;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.discover__chart-axis-on { color: var(--color-primary); font-weight: 600; }
.discover__chart-col--on .discover__chart-value {
color: var(--color-primary);
font-weight: 600;
}
.discover__chart-verdict { display: flex; align-items: center; gap: 6px; font-size: 0.75rem; }
.discover__chart-verdict--ok { color: var(--color-success); }
.discover__chart-verdict--warn { color: var(--color-warning); }
.discover__chart-verdict--bad { color: var(--color-error); }
/* --- Narrow ------------------------------------------------------------- */
/* Below this the two columns stop being two columns. With a model selected the
pane becomes the page and the rail steps aside, which is the same trade a
pushed route would make without the routing. */
@media (max-width: 900px) {
.split-view { grid-template-columns: minmax(0, 1fr); }
.entity-rail__list { max-height: 320px; }
.split-view--detail .split-view__rail-col { display: none; }
}
/* Compact list rows inside a pane: a state chip, a name, and one number. */
.rowlist { display: flex; flex-direction: column; gap: 4px; }
.rowline {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: 5px var(--spacing-sm);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
font-size: 0.8125rem;
color: var(--color-text-secondary);
}
.rowline__num { margin-left: auto; font-variant-numeric: tabular-nums; }
/* A first run is a whole screen, not a card wedged under a filter bar. These
two states are the only thing on the page, so they get the height to say so.
They previously wore .loading-center, which is display:flex with the default
row direction because it exists to centre one spinner. With four children
that put the icon, the heading, the sentence and the buttons on a single
line with no gap between them. */
.empty-state--page {
min-height: 52vh;
justify-content: center;
align-items: center;
text-align: center;
padding-block: var(--spacing-3xl);
}
.empty-state--page .empty-state-icon { font-size: 2rem; }
.empty-state--page .empty-state-text { text-wrap: balance; }
/* ==========================================================================
Full-height view: the split fills the window instead of sitting in a
document that scrolls. Rail and pane scroll independently, so the filters
and the pane's headline both stay put while a long list moves under them.
========================================================================== */
.page--app {
display: flex;
flex-direction: column;
flex: 1;
/* A flex item's automatic minimum is its content, so without this the list
pushes the page taller instead of scrolling inside it. */
min-height: 0;
padding-bottom: var(--spacing-lg);
/* The page scrolls inside the pinned shell; the rail and pane keep their own
inner scrollers for long lists and long details. */
overflow-y: auto;
}
/* The header, fused. A slim row that reads as the top of the view rather than
a title block the view happens to sit under. */
.view-bar {
display: flex;
align-items: baseline;
gap: var(--spacing-sm);
padding-bottom: var(--spacing-md);
flex-wrap: wrap;
}
.view-bar__title {
font-size: var(--text-3xl);
font-weight: var(--font-weight-bold);
letter-spacing: -0.03em;
line-height: 1.1;
}
.view-bar__count {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
}
.view-bar__actions { display: flex; gap: var(--spacing-sm); margin-left: auto; }
.page--app .split-view { flex: 1; min-height: 0; }
.page--app .split-view__rail-col {
height: 100%;
min-height: 0;
}
/* The filters keep their height; the list absorbs the rest and scrolls. */
.page--app .entity-rail {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.page--app .entity-rail__list {
flex: 1;
min-height: 0;
max-height: none;
}
.page--app .split-view__pane {
/* The pane keeps its own scroller. Letting it grow instead would push the
document taller and stretch the rail to match, which is the regression
e2e/discover-height.spec.js exists to catch. Reaching tall page chrome is
the page's job (overflow-y above), not the pane's. */
height: 100%;
overflow-y: auto;
}
.split-view__rail-col .filter-bar-group__toggle {
justify-content: flex-start;
width: 100%;
padding: var(--spacing-xs) 0;
}
@media (max-width: 900px) {
/* Stacked, the view cannot be height-constrained without trapping the list
in a short scroller, so it goes back to scrolling with the document. */
.page--app { display: block; }
.page--app .split-view__pane { height: auto; overflow: visible; }
.page--app .entity-rail__list { max-height: 320px; }
}
/* Pin the layout for split-view routes, the way the chat route already does.
.app-layout and .main-content are min-height:100dvh, which is a floor, not a
ceiling: a tall pane grew the whole column past the viewport, so selecting a
model with a long detail pushed the page down and the rail grew with it. The
"full height" view was scrolling the document instead of scrolling inside
itself.
:has() rather than a route flag in App.jsx, so the shell stays unaware of
which pages happen to be split views. */
.app-layout:has(.page--app) {
height: 100vh;
height: 100dvh;
}
.app-layout:has(.page--app) .main-content {
min-height: 0;
overflow: hidden;
}
/* Below the split's breakpoint the two columns stack, and a stacked view
cannot be height-pinned without trapping both halves in short scrollers. It
goes back to scrolling with the document. */
@media (max-width: 900px) {
.app-layout:has(.page--app) {
height: auto;
min-height: 100dvh;
}
.app-layout:has(.page--app) .main-content {
min-height: 100dvh;
overflow: visible;
}
}
.page--app > .tabs { flex: none; }
.page--app > .view-bar { flex: none; }
/* A split view needs a ceiling to scroll inside: without it the page ran past
the viewport and over the footer. The hub layout sizes to content for every
other page, so only the split-view routes are pinned, scoped with :has(). */
.hub-layout:has(.page--app) {
flex: 1;
min-height: 0;
}
.hub-layout:has(.page--app) > .hub-body {
min-height: 0;
display: flex;
flex-direction: column;
}
@media (max-width: 900px) {
.hub-layout:has(.page--app) { flex: none; }
.hub-layout:has(.page--app) > .hub-body { display: block; }
}
/* Refetching is not the same event as first load. A first load has nothing to
show, so it gets a skeleton; a refetch already has a list on screen and must
keep it, because the search box lives in this column and unmounting it drops
the field mid-keystroke along with the focus. */
.entity-rail__progress {
height: 2px;
background: transparent;
overflow: hidden;
flex: none;
}
.entity-rail--busy .entity-rail__progress {
background: linear-gradient(
90deg,
transparent 0%,
var(--color-primary) 40%,
var(--color-primary) 60%,
transparent 100%
);
background-size: 40% 100%;
background-repeat: no-repeat;
animation: entity-rail-sweep 1.1s var(--ease-default) infinite;
}
@keyframes entity-rail-sweep {
from { background-position: -40% 0; }
to { background-position: 140% 0; }
}
/* The stale list recedes rather than disappearing, so the eye knows the answer
is being replaced without losing the place it was reading. */
.entity-rail--busy .entity-rail__list {
opacity: 0.55;
transition: opacity var(--duration-normal) var(--ease-default);
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.entity-rail--busy .entity-rail__progress { animation: none; background-size: 100% 100%; }
.entity-rail--busy .entity-rail__list { transition: none; }
}
/* The first-load skeleton. It was six inline style declarations on a bare div,
which is also why nothing could reliably select it. */
.gallery-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-xl) var(--spacing-md);
min-height: 280px;
gap: var(--spacing-lg);
}
/* An estimate still in flight. Muted and gently pulsing, so the row reads as
working rather than as missing a value: what is pending is the "will it fit"
answer, not the entry itself, which is installable already. */
.entity-rail__meta--pending {
color: var(--color-text-tertiary);
font-style: italic;
animation: entity-rail-pending 1.6s var(--ease-default) infinite;
}
@keyframes entity-rail-pending {
0%, 100% { opacity: 0.55; }
50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.entity-rail__meta--pending { animation: none; opacity: 0.75; }
}
/* Pills are gone. A capsule radius reads as a tag floating on the surface,
which fights a system whose structure is hairlines and square corners — and
with chips on Models, Activity and the biometrics pages, "some pages
have pills" was the actual inconsistency. Round buttons keep their radius:
.lightbox__nav and .home-send-btn are circles, not capsules. */
/* ─── Lanes ───────────────────────────────────────────────────────────────
The list idiom carried over from localai.io: full-bleed rows separated by a
single hairline, insetting on hover, with no card and no shadow. Used
wherever records are flat, uniform and read in sequence. Where an entity
outgrows a row and candidates get compared before acting, use SplitView
instead — that is the other half of the rule and they should not be mixed.
Column templates belong to the caller: `.lanes--<name> .lane { grid-template-columns: ... }`. */
.lanes {
list-style: none;
margin: 0;
padding: 0;
border-top: 1px solid var(--color-border-default);
}
.lane {
display: grid;
align-items: center;
gap: var(--spacing-md);
width: 100%;
padding: var(--spacing-md) var(--spacing-xs);
border: 0;
border-bottom: 1px solid var(--color-border-default);
background: transparent;
color: inherit;
text-align: left;
font: inherit;
text-decoration: none;
transition: background var(--duration-fast) var(--ease-default),
padding var(--duration-fast) var(--ease-default);
}
a.lane, button.lane { cursor: pointer; }
a.lane:hover,
button.lane:hover {
background: var(--color-bg-hover);
padding-inline: var(--spacing-sm);
}
.lane:focus-visible { outline: 2px solid var(--color-focus-ring); outline-offset: -2px; }
/* A 33px row is fine for a mouse and too small for a thumb. Matches the rule
EntityRail already applies, so the two list idioms feel the same on touch. */
@media (pointer: coarse) {
.lane { padding-top: 12px; padding-bottom: 12px; }
}
/* The hover inset is a position change, so it is motion. Hold the background
feedback and drop the movement. */
@media (prefers-reduced-motion: reduce) {
.lane { transition: background var(--duration-fast) var(--ease-default); }
a.lane:hover, button.lane:hover { padding-inline: var(--spacing-xs); }
}
/* Uppercase micro-label. Blue where it names a route, mint where it reports
live state — never both on one screen. */
.lane__tag {
color: var(--dk-accent-text);
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.lane__main { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.lane__name { font-weight: var(--font-weight-medium); color: var(--color-text-primary); }
/* Identifiers are monospace, prose is not. A model id, a backend name or a
filename is a thing you might type or paste, and setting it in the UI face
makes it read as a label instead. */
.lane__name--id { font-family: var(--font-mono); font-size: var(--text-sm); font-weight: var(--font-weight-medium); }
.lane__desc { color: var(--color-text-muted); font-size: var(--text-base); }
/* Identifiers and quantities are monospace; prose is not. */
.lane__num {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-text-tertiary);
font-variant-numeric: tabular-nums;
overflow: hidden;
text-overflow: ellipsis;
}
.lane__go { color: var(--color-text-tertiary); }
/* Model templates: one lane per starting point. */
.template-picker { padding: 0 var(--spacing-lg) var(--spacing-lg); max-width: 56rem; }
.template-picker__lede { font-size: var(--text-base); color: var(--color-text-secondary); margin-bottom: var(--spacing-md); }
.lanes--templates .lane { grid-template-columns: 1.75rem minmax(0, 1fr) auto; align-items: start; }
.template-picker__icon { font-size: 1.25rem; color: var(--color-text-secondary); margin-top: 2px; }
.lanes--templates .lane__name { font-size: var(--text-md, 1rem); font-weight: var(--font-weight-semibold); }
.lanes--templates .lane__desc { font-size: var(--text-sm); }
.template-picker__fields {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-text-tertiary);
overflow-wrap: anywhere;
}
.lanes--templates .lane__go { margin-top: 2px; }
/* Section rhythm above a set of lanes. */
.lane-head {
display: flex;
align-items: baseline;
gap: var(--spacing-sm);
margin: var(--spacing-2xl) 0 var(--spacing-sm);
}
/* No :first-child reset. Every <section> makes its lane-head a first child, so
the reset silently killed the gap between every block — "Sections" sat flush
against the row above it. The header supplies its own bottom margin, so a
uniform top margin is right everywhere. */
.lane-head h2 { margin: 0; font-size: var(--text-lg); font-weight: var(--font-weight-bold); letter-spacing: -0.01em; }
.lane-head__meta {
margin-left: auto;
color: var(--color-text-tertiary);
font-family: var(--font-mono);
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
/* Standard padding for pages that are not inside a hub layout, which
supplies its own. Without this a top-level page sits flush against the
sidebar and its first character is clipped. */
.page-pad {
padding: var(--spacing-xl);
max-width: var(--page-max-medium);
width: 100%;
}
.lanes--usecase .lane { grid-template-columns: minmax(0, 10rem) minmax(0, 1fr) auto; }
.lanes--usecase .lane__tag { display: inline-flex; align-items: center; gap: var(--spacing-xs); }
/* The request the form just built. Sunken and monospace: it is a record of what
was sent, not another control. */
.request-panel {
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-sunken);
overflow: hidden;
margin-bottom: var(--spacing-md);
}
.request-panel__head {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: 4px var(--spacing-sm);
border-bottom: 1px solid var(--color-border-default);
}
.request-panel__title {
font-size: 0.625rem;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--color-text-muted);
}
.request-panel__copy {
margin-left: auto;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
font-size: 0.6875rem;
padding: 2px 8px;
cursor: pointer;
}
.request-panel__copy:hover { color: var(--color-primary); border-color: var(--color-primary-border); }
.request-panel__copy:focus-visible { outline: 2px solid var(--color-focus-ring); outline-offset: 1px; }
.request-panel__code {
margin: 0;
padding: var(--spacing-sm);
overflow-x: auto;
font-family: var(--font-mono);
font-size: 0.6875rem;
line-height: 1.6;
color: var(--color-text-secondary);
text-wrap: auto;
}
.request-panel__method { color: var(--color-text-tertiary); }
.request-panel__endpoint { color: var(--color-primary); font-weight: 500; }
/* A row of page-header controls. Replaces the inline flex that several pages
had each written out longhand. */
.header-actions {
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
/* The file input is triggered by its label, which is styled as a button; the
input itself must not render or the control shows twice. */
.agents-import-input input[type="file"] { display: none; }
/* Recommended models: ranked candidates, so lanes in fit order rather than a
grid of equal cards. The leader carries the one amber label on the page. */
.lanes--recommended .lane { grid-template-columns: minmax(0, 1fr) auto; }
.lanes--recommended .lane__name { overflow-wrap: anywhere; }
.lane__tag--evidence { color: var(--color-warning); }
/* ─── Studio: capability dots ─────────────────────────────────────────────
Filled means a model on this machine serves that modality; hollow means
none does. Both are drawn in text colours, because a status colour on
every tab competes with the one thing the row is for: picking a type.
The dot is aria-hidden and the tab carries the same fact as hidden text. */
.studio-tab__dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex: none;
margin-left: var(--spacing-xs);
}
.studio-tab__dot--on { background: var(--color-text-tertiary); }
.studio-tab__dot--off {
background: transparent;
box-shadow: inset 0 0 0 1px var(--color-border-strong);
}
.studio-overview__heading { display: flex; align-items: baseline; gap: var(--spacing-sm); }
.studio-overview__count {
margin-left: auto;
color: var(--color-text-tertiary);
font-family: var(--font-mono);
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
.animation-viewer canvas {
width: 100%;
height: 420px;
touch-action: none;
cursor: grab;
}
/* ---- Icons (shared kit outline set, see components/Icon.jsx) ---------- */
.lai-icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
/* The kit draws on a 24 unit grid with a 2 unit margin, so at 1em the mark
looks about a sixth smaller than a font glyph did. 1.2em gives the same
visual size, and the em unit keeps every icon following its context's font
size. The line is a little heavier than the kit's 1.75 so it holds up at the
12-16px sizes the app uses. */
:where(.lai-icon) {
display: inline-block;
width: 1.2em;
height: 1.2em;
vertical-align: -0.25em;
flex: none;
}
.lai-icon--glyph { fill: currentColor; stroke: none; }
.lai-icon:not(.lai-icon--glyph) { fill: none; stroke: currentColor; stroke-width: var(--lai-stroke, 2); stroke-linecap: round; stroke-linejoin: round; }
/* Large marks (empty states, heroes) read heavier at the same line weight. */
.empty-state-icon.lai-icon,
.empty-state-icon .lai-icon,
.biometrics-empty > .lai-icon{ --lai-stroke: 1.5; }
.lai-icon--beat { animation: lai-icon-beat 1s ease-in-out infinite; }
@keyframes lai-icon-beat {
50% { transform: scale(1.18); }
}
@media (prefers-reduced-motion: reduce) {
.lai-icon--beat { animation: none; }
}
/* Pinned apps under the Apps entry: their labels line up with the labels above them. */
.sidebar-nav .nav-item.nav-item--sub { padding-inline-start: 36px; }