mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 17:18:49 -04:00
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)
9711 lines
273 KiB
CSS
9711 lines
273 KiB
CSS
/* 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; }
|