/* 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-secondary); 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); } .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(--duration-fast) var(--ease-default); } .mobile-header-btn:hover { background: var(--color-bg-hover); } .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 .fa-user-circle { font-size: 1.5rem; } /* Sidebar */ .sidebar { position: fixed; top: 0; left: 0; width: var(--sidebar-width); height: 100vh; height: 100dvh; background: var(--color-bg-secondary); border-right: 1px solid var(--color-border-subtle); 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-sm); border-bottom: 1px solid var(--color-border-subtle); min-height: 44px; } .sidebar-logo-link { display: block; } .sidebar-logo-img { width: 100%; max-width: 120px; height: auto; padding: 0 var(--spacing-xs); } .sidebar-logo-icon { display: none; } .sidebar-logo-icon-img { width: 28px; height: 28px; } .sidebar-close-btn { display: none; background: none; border: none; color: var(--color-text-secondary); font-size: 1.25rem; cursor: pointer; } .sidebar-nav { flex: 1; padding: 2px 0; overflow-y: auto; } .sidebar-section { padding: 2px 0; } .sidebar-section-title { padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-xs); font-size: var(--text-xs); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-secondary); 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(--duration-fast); } .sidebar-section-toggle:hover { color: var(--color-text-primary); } .sidebar-section-chevron { font-size: 0.5rem; opacity: 0.6; transition: transform var(--duration-fast), opacity var(--duration-fast); 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); padding: 8px var(--spacing-md) 8px calc(var(--spacing-sm) + 2px); color: var(--color-text-secondary); text-decoration: none; font-size: var(--text-sm); font-weight: var(--font-weight-medium); transition: color var(--duration-normal) var(--ease-spring), background var(--duration-normal) var(--ease-spring), box-shadow var(--duration-normal) var(--ease-spring); white-space: nowrap; overflow: hidden; } .nav-item:hover:not(.active) { color: var(--color-text-primary); background: var(--color-surface-elevated); } .nav-item.active { color: var(--color-primary); background: var(--color-primary-light); box-shadow: inset 2px 0 0 var(--color-primary); font-weight: var(--font-weight-medium); } .nav-icon { width: 18px; text-align: center; flex-shrink: 0; font-size: 0.85rem; } .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 (reused by the admin console rail) keeps its own treatment. */ .sidebar-nav .nav-item { margin: 1px var(--spacing-sm); padding-left: var(--spacing-sm); padding-right: var(--spacing-sm); border-radius: var(--radius-lg); } .sidebar-nav .nav-item.active { background: var(--color-primary-light); color: var(--color-primary); font-weight: var(--font-weight-medium); box-shadow: none; } .sidebar-nav .nav-item.active .nav-icon { color: var(--color-primary); } /* Align tier labels with the inset item text (item margin + icon padding). */ .sidebar-nav .sidebar-section-title { padding-left: var(--spacing-md); padding-right: var(--spacing-md); } .nav-external { font-size: 0.55rem; margin-left: auto; opacity: 0.5; flex-shrink: 0; } .sidebar-footer { padding: var(--spacing-xs) var(--spacing-sm); border-top: 1px solid var(--color-border-subtle); 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: 0.75rem; color: var(--color-text-secondary); overflow: hidden; } .sidebar-user-avatar { width: 20px; height: 20px; border-radius: var(--radius-full); flex-shrink: 0; } .sidebar-user-avatar-icon { font-size: 1.25rem; 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; background: none; border: none; padding: 2px var(--spacing-xs); margin: -2px calc(-1 * var(--spacing-xs)); border-radius: var(--radius-sm); color: inherit; font: inherit; cursor: pointer; transition: background var(--duration-fast), color var(--duration-fast); } .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 { flex: 0; margin: 0; padding: 2px; } .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: 32px; height: 32px; background: none; border: none; color: var(--color-text-muted); cursor: pointer; border-radius: var(--radius-md); font-size: var(--text-sm); transition: color var(--duration-fast), background var(--duration-fast); flex-shrink: 0; } .sidebar-collapse-btn:hover { color: var(--color-text-primary); background: var(--color-surface-hover); } /* 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; } .sidebar.collapsed .nav-icon { width: auto; font-size: 1rem; } .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: 4px; font-size: 0.75rem; } .sidebar.collapsed .theme-toggle .nav-label { display: none; } /* Theme toggle */ .theme-toggle { background: none; border: 1px solid var(--color-border-subtle); color: var(--color-text-secondary); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius-md); cursor: pointer; font-size: 0.875rem; transition: color var(--duration-fast) var(--ease-default), border-color var(--duration-fast) var(--ease-default), background var(--duration-fast) var(--ease-default); } .theme-toggle:hover { color: var(--color-primary); border-color: var(--color-primary-border); } .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; } .language-switcher-code { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em; } .sidebar.collapsed .language-switcher-code { display: none; } .language-switcher-menu { position: absolute; bottom: calc(100% + 8px); left: 0; min-width: 160px; background: var(--color-bg-elevated, var(--color-bg-secondary)); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); box-shadow: var(--shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.18)); list-style: none; margin: 0; padding: 4px; z-index: 1000; } .sidebar.collapsed .language-switcher-menu { left: calc(100% + 8px); bottom: 0; } .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); color: var(--color-text-secondary); cursor: pointer; border-radius: var(--radius-sm); font-size: 0.875rem; text-align: left; } .language-switcher-option:hover { background: var(--color-bg-tertiary, rgba(255, 255, 255, 0.04)); color: var(--color-text-primary); } .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, #111); } .app-boot-spinner-dot { width: 36px; height: 36px; border-radius: 50%; border: 3px solid var(--color-border-subtle, rgba(255, 255, 255, 0.15)); border-top-color: var(--color-primary, #4f8cff); 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); border-left: 2px solid var(--color-primary); 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 { border-left-color: var(--color-error); background: var(--color-error-light); } .operations-strip--queued { border-left-color: var(--color-text-disabled); } .operations-strip--staging { border-left-color: var(--color-info); } .operations-strip--removing { border-left-color: var(--color-warning); } .operations-strip--done { border-left-color: var(--color-success); } .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; } /* Per-node breakdown of a multi-worker install. */ .operation-nodes-list { list-style: none; margin: var(--spacing-xs) 0 0; padding: var(--spacing-xs) 0 0; border-top: 1px solid var(--color-border-subtle); flex-basis: 100%; width: 100%; } .operation-node { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-xs) 0; font-size: var(--text-xs); color: var(--color-text-muted); flex-wrap: wrap; } .operation-node-status { padding: 2px 6px; border-radius: var(--radius-md); font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; white-space: nowrap; } .operation-node-status-success { background: var(--color-success-light); color: var(--color-success); } .operation-node-status-error { background: var(--color-error-light); color: var(--color-error); } .operation-node-status-queued { background: var(--color-bg-tertiary); color: var(--color-text-muted); } .operation-node-status-running_on_worker { background: var(--color-warning-light); color: var(--color-warning); } .operation-node-status-downloading { background: var(--color-primary-light); color: var(--color-primary); } .operation-node-name { font-weight: 500; color: var(--color-text-secondary); } .operation-node-file { font-family: var(--font-mono); color: var(--color-text-tertiary); overflow: hidden; text-overflow: ellipsis; max-width: 30ch; white-space: nowrap; } .operation-node-bytes { font-variant-numeric: tabular-nums; color: var(--color-text-tertiary); } .operation-node-pct { font-variant-numeric: tabular-nums; color: var(--color-primary); font-weight: 500; } .operation-node-error { color: var(--color-error); } .operation-node-bar-container { flex-basis: 100%; height: 3px; background: var(--color-surface-sunken); border-radius: var(--radius-full); overflow: hidden; margin-top: 0.25rem; } .operation-node-bar { height: 100%; background: var(--color-primary); border-radius: var(--radius-full); transition: width var(--duration-slow, 0.3s) var(--ease-spring, ease); } /* Toast */ .toast-container { position: fixed; top: var(--spacing-lg); right: var(--spacing-lg); z-index: 1100; display: flex; flex-direction: column; gap: var(--spacing-sm); } .toast { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-secondary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); color: var(--color-text-primary); font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: -0.005em; animation: toastSlideIn var(--duration-normal) var(--ease-spring); min-width: 280px; } .toast-enter { opacity: 0; transform: translateX(12px); } .toast-exit { opacity: 0; transform: translateX(12px); transition: opacity var(--duration-fast) var(--ease-spring), transform var(--duration-fast) var(--ease-spring); } .toast-success { box-shadow: inset 3px 0 0 var(--color-success), var(--shadow-sm); border-color: var(--color-border-subtle); } .toast-error { box-shadow: inset 3px 0 0 var(--color-error), var(--shadow-sm); border-color: var(--color-border-subtle); } .toast-warning { box-shadow: inset 3px 0 0 var(--color-warning), var(--shadow-sm); border-color: var(--color-border-subtle); } .toast-info { box-shadow: inset 3px 0 0 var(--color-info), var(--shadow-sm); border-color: var(--color-border-subtle); } .toast-close { margin-left: auto; background: none; border: none; color: inherit; opacity: 0.6; cursor: pointer; padding: 2px; } .toast-close:hover { opacity: 1; } .toast-link { font-size: 0.75rem; color: inherit; opacity: 0.8; 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( 100deg, var(--color-surface-sunken) 30%, var(--color-surface-hover) 50%, var(--color-surface-sunken) 70% ); background-size: 200% 100%; animation: skeletonShimmer 1.4s ease-in-out 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); } /* Resource monitor */ .resource-monitor { background: var(--color-bg-secondary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); padding: var(--spacing-md); } .resource-monitor-title { font-size: 0.875rem; color: var(--color-text-secondary); margin-bottom: var(--spacing-sm); } .resource-gpu-list { display: flex; flex-direction: column; gap: var(--spacing-sm); } .resource-gpu-card { background: var(--color-bg-tertiary); border-radius: var(--radius-md); padding: var(--spacing-sm); } .resource-gpu-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs); } .resource-gpu-name { font-size: 0.8125rem; font-weight: 500; color: var(--color-text-primary); } .resource-gpu-vendor { font-size: 0.6875rem; padding: 2px 6px; background: var(--color-accent-light); color: var(--color-accent); border-radius: var(--radius-sm); } .resource-gpu-stats { display: flex; gap: var(--spacing-md); font-size: 0.75rem; color: var(--color-text-muted); margin-top: var(--spacing-xs); } .resource-bar-container { height: 4px; background: var(--color-bg-primary); border-radius: 2px; overflow: hidden; } .resource-bar { height: 100%; background: var(--color-primary); border-radius: 2px; transition: width 500ms ease; } .resource-bar-ram { background: var(--color-secondary); } .resource-no-gpu { font-size: 0.8125rem; color: var(--color-text-muted); padding: var(--spacing-sm); } .resource-ram { margin-top: var(--spacing-sm); } .resource-ram-header { display: flex; justify-content: space-between; font-size: 0.8125rem; color: var(--color-text-secondary); margin-bottom: var(--spacing-xs); } .resource-monitor-compact { display: flex; gap: var(--spacing-md); flex-wrap: wrap; } .resource-item { display: flex; align-items: center; gap: var(--spacing-xs); font-size: 0.75rem; color: var(--color-text-muted); } /* 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: clamp(1.875rem, 1.3rem + 2.2vw, 3rem); font-weight: var(--font-weight-semibold); letter-spacing: -0.04em; line-height: 1.02; 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-medium); letter-spacing: -0.005em; 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__eyebrow { font-family: var(--font-mono); font-size: var(--text-xs); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.14em; color: var(--color-eyebrow); display: inline-flex; align-items: center; gap: var(--spacing-xs); } .page-header__eyebrow::before { content: ''; width: 18px; height: 1px; background: var(--color-eyebrow); opacity: 0.7; } .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-subtle); border-radius: var(--radius-lg); padding: var(--spacing-lg); box-shadow: var(--shadow-subtle), var(--shadow-inset-top); transition: border-color var(--duration-normal) var(--ease-spring), box-shadow var(--duration-normal) var(--ease-spring), transform var(--duration-normal) var(--ease-spring); } .card:hover { border-color: var(--color-border-strong); box-shadow: var(--shadow-sm), var(--shadow-inset-top); transform: translateY(-1px); } /* Accent-rail variant — editorial left bar for highlighted cards */ .card--accent { box-shadow: inset 2px 0 0 var(--color-primary), var(--shadow-subtle), var(--shadow-inset-top); } .card--accent:hover { box-shadow: inset 2px 0 0 var(--color-primary), var(--shadow-sm), var(--shadow-inset-top); } .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-sm); font-weight: var(--font-weight-medium); color: var(--color-text-primary); } .form-row__hint { font-size: var(--text-xs); 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 i { 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%; } /* Progress bar */ .progress-bar { width: 100%; height: 24px; border-radius: var(--radius-full); background: var(--color-surface-sunken); overflow: hidden; border: 1px solid var(--color-border-subtle); } .progress-bar__fill { height: 100%; background: var(--color-primary); display: flex; align-items: center; justify-content: center; font-size: var(--text-xs); font-weight: var(--font-weight-semibold); color: var(--color-primary-text); transition: width 300ms var(--ease-default); white-space: nowrap; padding: 0 var(--spacing-sm); } .progress-bar__fill--error { background: var(--color-error); } /* Log tail viewport */ .log-tail { max-height: 180px; overflow: auto; background: var(--color-surface-sunken); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); padding: var(--spacing-sm) var(--spacing-md); font-family: var(--font-mono); font-size: var(--text-xs); line-height: var(--leading-snug); color: var(--color-text-secondary); } .log-tail__line { padding: 1px 0; } .log-tail__line--error { color: var(--color-error); } /* Result quote (TTS / Sound prompt echo) */ .result-quote { padding: var(--spacing-md); background: var(--color-surface-sunken); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); color: var(--color-text-secondary); font-style: italic; text-align: center; line-height: var(--leading-normal); } /* 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; } /* Quantize page */ .quantize-page__header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--spacing-md); } .quantize-form { margin-bottom: var(--spacing-lg); } .quantize-form__quant-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-sm); } .quantize-form__quant-row > :only-child { grid-column: 1 / -1; } @media (max-width: 520px) { .quantize-form__quant-row { grid-template-columns: 1fr; } } .quantize-progress-card { margin-bottom: var(--spacing-lg); } .quantize-progress-card__header { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--spacing-md); margin-bottom: var(--spacing-md); } .quantize-progress-card__title { display: flex; align-items: center; gap: var(--spacing-sm); margin: 0; font-size: var(--text-base); color: var(--color-text-primary); } .quantize-progress-card__title i { color: var(--color-primary); } .quantize-progress-card__status { display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-md); flex-wrap: wrap; } .quantize-progress-card__message { font-size: var(--text-sm); color: var(--color-text-secondary); } .quantize-progress-card .progress-bar { margin-bottom: var(--spacing-md); } .quantize-import-card { margin-bottom: var(--spacing-lg); } .quantize-import-card__title { display: flex; align-items: center; gap: var(--spacing-sm); margin: 0 0 var(--spacing-md); font-size: var(--text-base); color: var(--color-text-primary); } .quantize-import-card__title i { color: var(--color-primary); } .quantize-import-card__row { display: flex; gap: var(--spacing-sm); flex-wrap: wrap; align-items: center; } .quantize-import-card__name { flex: 1; min-width: 220px; max-width: 320px; } .quantize-jobs { padding: 0; overflow: hidden; } .quantize-jobs__title { display: flex; align-items: center; gap: var(--spacing-sm); margin: 0; padding: var(--spacing-md) var(--spacing-lg); border-bottom: 1px solid var(--color-border-divider); font-size: var(--text-base); color: var(--color-text-primary); } .quantize-jobs__title i { color: var(--color-primary); } .quantize-jobs__scroll { overflow-x: auto; } /* Segmented control (Sound mode toggle etc.) */ .segmented { display: inline-flex; padding: 3px; background: var(--color-surface-sunken); border: 1px solid var(--color-border-subtle); 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(--duration-fast), color var(--duration-fast); } .segmented__item:hover { color: var(--color-text-primary); } .segmented__item.is-active { background: var(--color-surface-raised); color: var(--color-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(--duration-fast); 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; } /* Audio result wrapper (TTS/Sound) */ .audio-result { display: flex; flex-direction: column; align-items: stretch; gap: var(--spacing-md); width: 100%; max-width: 480px; } .audio-result__player { width: 100%; } .audio-result__actions { display: flex; gap: var(--spacing-sm); justify-content: center; } /* Media empty state */ .media-empty { text-align: center; color: var(--color-text-muted); padding: var(--spacing-xl) var(--spacing-md); } .media-empty__icon { display: block; font-size: 2.75rem; margin-bottom: var(--spacing-md); opacity: 0.35; } .media-empty p { font-size: var(--text-sm); margin: 0; } /* Buttons */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--spacing-xs); padding: 0.5rem var(--spacing-md); min-height: 34px; border-radius: var(--radius-md); font-size: var(--text-sm); font-family: inherit; font-weight: var(--font-weight-medium); letter-spacing: -0.005em; cursor: pointer; border: 1px solid transparent; transition: background var(--duration-normal) var(--ease-spring), color var(--duration-normal) var(--ease-spring), border-color var(--duration-normal) var(--ease-spring), box-shadow var(--duration-normal) var(--ease-spring), filter var(--duration-normal) var(--ease-spring), transform var(--duration-normal) var(--ease-spring); text-decoration: none; white-space: nowrap; } .btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--color-focus-ring); } /* Global focus ring - any interactive that isn't a .btn. This box-shadow ring is the single focus technique app-wide (it covers .btn plus the :where(...) list below). The strengthened --color-focus-ring keeps it WCAG-AA visible (>=3:1), so no separate solid-outline rule is needed; a bare :focus-visible outline here would double-ring every element. */ :where(a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])):focus-visible { outline: none; box-shadow: 0 0 0 3px var(--color-focus-ring); border-radius: var(--radius-sm); } .btn-primary { background: var(--color-primary); color: var(--color-primary-text); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25), var(--shadow-inset-hi); } .btn-primary:hover:not(:disabled) { filter: brightness(1.06); transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), var(--shadow-inset-hi); } .btn-primary:focus-visible:not(:disabled) { box-shadow: 0 0 0 3px var(--color-focus-ring), var(--shadow-inset-hi); } .btn-secondary { background: var(--color-surface-elevated); color: var(--color-text-primary); border-color: var(--color-border-default); } .btn-secondary:hover:not(:disabled) { border-color: var(--color-border-strong); background: var(--color-surface-hover); transform: translateY(-1px); } .btn-ghost { background: transparent; color: var(--color-text-secondary); border-color: transparent; } .btn-ghost:hover:not(:disabled) { background: var(--color-surface-elevated); color: var(--color-text-primary); } .btn-danger { background: var(--color-error-light); color: var(--color-error); border-color: var(--color-error-border); } .btn-danger:hover:not(:disabled) { background: var(--color-error); color: var(--color-text-inverse); border-color: var(--color-error); filter: brightness(1.04); transform: translateY(-1px); } .btn-sm { padding: 0.35rem var(--spacing-sm); min-height: 28px; font-size: var(--text-xs); letter-spacing: 0; } .btn:active:not(:disabled) { filter: brightness(0.95); transform: translateY(0); } .btn:disabled { opacity: 0.45; cursor: not-allowed; filter: none; transform: none; } /* 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 var(--duration-normal) var(--ease-default); } .toggle__thumb { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: var(--radius-full); background: #ffffff; box-shadow: var(--shadow-sm); transition: transform var(--duration-normal) var(--ease-default); } .toggle--on .toggle__track { background: var(--color-toggle-on); } .toggle--on .toggle__thumb { transform: translateX(16px); } .toggle:hover:not(.toggle--disabled) .toggle__track { filter: brightness(1.08); } .toggle:focus-within .toggle__track { box-shadow: 0 0 0 3px var(--color-border-focus); } .toggle--disabled { cursor: not-allowed; opacity: 0.5; } /* Inputs — sunken well, quiet border, sage focus ring */ .input { background: var(--color-surface-sunken); color: var(--color-text-primary); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); padding: var(--spacing-sm) var(--spacing-md); font-size: var(--text-sm); font-family: inherit; letter-spacing: -0.005em; outline: none; width: 100%; transition: border-color var(--duration-normal) var(--ease-spring), box-shadow var(--duration-normal) var(--ease-spring), background var(--duration-normal) var(--ease-spring); } .input::placeholder { color: var(--color-text-muted); opacity: 0.8; } .input:hover:not(:disabled):not(:focus) { border-color: var(--color-border-strong); } .input:focus { border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-focus-ring); background: var(--color-surface-sunken); } .input:disabled { background: var(--color-surface-sunken); color: var(--color-text-muted); cursor: not-allowed; opacity: 0.7; } 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-surface-sunken); color: var(--color-text-primary); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); padding: var(--spacing-sm) var(--spacing-md); font-size: var(--text-sm); font-family: inherit; letter-spacing: -0.005em; outline: none; width: 100%; resize: vertical; min-height: 80px; line-height: var(--leading-normal); transition: border-color var(--duration-normal) var(--ease-spring), box-shadow var(--duration-normal) var(--ease-spring); } .textarea::placeholder { color: var(--color-text-muted); opacity: 0.8; } .textarea:hover:not(:disabled):not(:focus) { border-color: var(--color-border-strong); } .textarea:focus { border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-focus-ring); } .textarea:disabled { background: var(--color-surface-sunken); color: var(--color-text-muted); cursor: not-allowed; opacity: 0.7; } /* 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: 0.8125rem; color: var(--color-text-secondary); margin-bottom: var(--spacing-xs); font-weight: 500; } /* Badges — sharp editorial rectangles, mono caps */ .badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: var(--radius-sm); 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(--color-success); } .badge-error { background: var(--color-error-light); color: var(--color-error); } .badge-info { background: var(--color-info-light); color: var(--color-info); } .badge-warning { background: var(--color-warning-light); color: var(--color-warning); } .badge-accent { background: var(--color-accent-light); color: var(--color-accent); } /* badge-link marks a badge that navigates somewhere (e.g. the use-case chips that open a playground page). Signals clickability the bare colour variant doesn't: pointer cursor, and a hover state that lifts the background/text so links read as interactive while non-link badges (Embeddings, VAD, Score, ...) stay flat. */ .badge-link { cursor: pointer; text-decoration: none; transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease; } .badge-link:hover, .badge-link:focus-visible { background: var(--color-info); color: var(--color-bg-primary); box-shadow: 0 0 0 1px var(--color-info); outline: none; } /* 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 > i { 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(--color-error); border-color: transparent; } .btn.btn-danger-ghost:hover:not(:disabled) { background: var(--color-error-light); color: var(--color-error); border-color: var(--color-error-light); } /* 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-sm); font-size: var(--text-xs); font-weight: 600; line-height: 1.4; } .tab-pill--warning { background: var(--color-warning-light); color: var(--color-warning); } /* Stat cards — uniform-height cluster metrics for the Nodes dashboard. Left accent bar ties the color to the metric's semantic (success/warning/ error/primary), icon chip sits top-right, 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::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--stat-accent, var(--color-border-subtle)); transition: background var(--duration-fast) var(--ease-default); } .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-xs); font-size: var(--text-xs); color: var(--color-text-secondary); cursor: pointer; user-select: none; white-space: nowrap; } /* Models gallery filter block. Three bands (query / taxonomy / refinements) stacked by .filter-bar-group. The refinements live in their own band so the chip row's wrap height can never displace them. */ .models-filters__query { flex-wrap: nowrap; gap: var(--spacing-sm); } .models-filters__backend { flex: 0 0 auto; min-width: 0; } /* .filter-bar carries its own bottom margin for standalone use; inside the group the parent's gap owns the rhythm. */ .models-filters > .filter-bar { margin-bottom: 0; } .models-filters__refine { display: flex; align-items: center; flex-wrap: wrap; gap: var(--spacing-sm) var(--spacing-lg); padding-top: var(--spacing-sm); border-top: 1px solid var(--color-border-subtle); } /* Both refinements read as one secondary group: same size, same colour and the same icon-then-label rhythm as .filter-bar-group__toggle. */ .models-filters__context { display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--text-xs); color: var(--color-text-secondary); } .models-filters__context > label { display: inline-flex; align-items: center; gap: var(--spacing-xs); white-space: nowrap; cursor: pointer; } .models-filters__context input[type='range'] { width: 140px; accent-color: var(--color-primary); cursor: pointer; } .models-filters__context-value { font-weight: var(--font-weight-medium); min-width: 3em; font-variant-numeric: tabular-nums; } @media (max-width: 768px) { /* Search and backend select each take a full row rather than squeezing the input below a usable width. */ .models-filters__query { flex-wrap: wrap; } .models-filters__backend { flex: 1 1 100%; } } .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; box-shadow: var(--shadow-lg); animation: popoverIn var(--duration-fast) var(--ease-default); } @keyframes popoverIn { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } .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 > i { 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; } .node-status__dot { width: 8px; height: 8px; border-radius: 50%; box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 15%, transparent); flex-shrink: 0; } /* 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 */ .tabs { display: flex; gap: 0; border-bottom: 1px solid var(--color-border-subtle); margin-bottom: var(--spacing-md); } .tab { padding: var(--spacing-sm) var(--spacing-md); background: none; border: none; color: var(--color-text-secondary); font-size: 0.875rem; font-family: inherit; cursor: pointer; border-bottom: 2px solid transparent; transition: all var(--duration-fast); } .tab:hover { color: var(--color-text-primary); } .tab-active { color: var(--color-primary); border-bottom-color: var(--color-primary); } /* 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 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; } .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); } /* Model checkbox list */ .model-list { display: flex; flex-direction: column; gap: 2px; max-height: 200px; overflow: auto; padding: var(--spacing-xs); background: var(--color-bg-secondary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); } .model-list::-webkit-scrollbar { width: 6px; } .model-list::-webkit-scrollbar-track { background: transparent; } .model-list::-webkit-scrollbar-thumb { background: var(--color-border-default); border-radius: var(--radius-full); } .model-item { display: flex; align-items: center; gap: var(--spacing-sm); padding: 6px var(--spacing-sm); cursor: pointer; border-radius: var(--radius-sm); transition: background var(--duration-fast) var(--ease-default); user-select: none; } .model-item:hover { background: var(--color-primary-light); } .model-item.model-item-checked { background: var(--color-primary-light); } .model-item input[type="checkbox"] { display: none; } .model-item-check { width: 18px; height: 18px; border-radius: var(--radius-sm); border: 2px solid var(--color-border-default); display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: all var(--duration-fast) var(--ease-default); background: transparent; } .model-item:hover .model-item-check { border-color: var(--color-primary); } .model-item-checked .model-item-check { background: var(--color-primary); border-color: var(--color-primary); box-shadow: 0 0 0 1px var(--color-primary-light); } .model-item-checked .model-item-check i { color: white; font-size: 10px; animation: checkPop var(--duration-fast) var(--ease-default); } @keyframes checkPop { 0% { transform: scale(0); } 60% { transform: scale(1.2); } 100% { transform: scale(1); } } .model-item-name { font-family: var(--font-mono); font-size: 0.8rem; color: var(--color-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .model-item-checked .model-item-name { color: var(--color-primary); } /* 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 i { transition: transform var(--duration-fast); } .collapsible-header.open i { transform: rotate(90deg); } /* Search bar */ .search-bar { position: relative; } .search-bar .search-icon { position: absolute; left: var(--spacing-sm); top: 50%; transform: translateY(-50%); color: var(--color-text-muted); font-size: 0.875rem; } .search-bar .input { padding-left: 2rem; } /* 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 { padding: 6px var(--spacing-md); background: var(--color-surface-raised); color: var(--color-text-secondary); border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); cursor: pointer; font-size: var(--text-xs); font-weight: var(--font-weight-medium); font-family: inherit; box-shadow: var(--shadow-subtle); transition: all var(--duration-fast); } .filter-btn:hover { border-color: var(--color-primary-border); color: var(--color-primary); transform: translateY(-1px); box-shadow: var(--shadow-sm); } .filter-btn.active { background: var(--color-primary-light); color: var(--color-primary); 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 { box-shadow: 0 0 0 3px var(--color-focus-ring); border-color: var(--color-primary); } /* 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; 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 { animation: pageReveal var(--duration-reveal) var(--ease-reveal) both; display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; } @keyframes pageReveal { from { opacity: 0; transform: translateY(10px); } 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); border-left: 3px solid transparent; transition: background var(--duration-fast), color var(--duration-fast), border-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); border-left-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-jump-latest { position: sticky; bottom: 12px; align-self: center; display: inline-flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-md); background: var(--color-surface-raised); color: var(--color-text-primary); border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); box-shadow: var(--shadow-md); font-size: var(--text-sm); font-weight: var(--font-weight-medium); cursor: pointer; z-index: 5; animation: pageReveal var(--duration-fast) var(--ease-default) both; } .chat-jump-latest:hover { border-color: var(--color-primary-border); color: var(--color-primary); } .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-left: 2px solid var(--color-border-strong); 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-left: 2px solid var(--color-primary); 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-xs) var(--spacing-md) var(--spacing-sm); background: var(--color-bg-secondary); border-top: 1px solid var(--color-border-subtle); } .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-default); border-radius: var(--radius-md); padding: var(--spacing-xs); 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. */ .chat-input-modes { display: flex; align-items: center; gap: var(--spacing-xs); flex-shrink: 0; } .chat-mode-chip { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; background: transparent; border: 1px solid var(--color-border-subtle); border-radius: 999px; color: var(--color-text-muted); font-family: inherit; font-size: 0.75rem; font-weight: 500; cursor: pointer; transition: background var(--duration-fast), border-color var(--duration-fast), color var(--duration-fast); } .chat-mode-chip:hover { background: var(--color-bg-hover); border-color: var(--color-border-default); color: var(--color-text-secondary); } .chat-mode-chip:focus-visible { outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } .chat-mode-chip-on, .chat-mode-chip-on:hover { background: var(--color-primary-light); border-color: var(--color-primary-border); color: var(--color-primary); } .chat-mode-chip i { font-size: 0.7rem; } .chat-mode-chip-label { line-height: 1; } .chat-mode-chip-count { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 16px; padding: 0 5px; margin-left: 2px; border-radius: 999px; background: var(--color-primary); color: var(--color-primary-text); font-size: 0.65rem; font-weight: 600; cursor: pointer; line-height: 1; } .chat-mode-chip-count:hover { filter: brightness(1.1); } /* MCP popover sits above the chip when it's anchored to the input row (otherwise it would drop off-screen). */ .chat-input-modes .chat-mcp-dropdown-menu { top: auto; bottom: calc(100% + 4px); } /* When the MCP dropdown lives inside the modes row, make its trigger match the chip aesthetic without altering the component itself. */ .chat-input-modes .chat-mcp-dropdown > .btn { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; background: transparent; border: 1px solid var(--color-border-subtle); border-radius: 999px; color: var(--color-text-muted); font-family: inherit; font-size: 0.75rem; font-weight: 500; box-shadow: none; text-transform: none; letter-spacing: 0; transition: background var(--duration-fast), border-color var(--duration-fast), color var(--duration-fast); } .chat-input-modes .chat-mcp-dropdown > .btn:hover { background: var(--color-bg-hover); border-color: var(--color-border-default); color: var(--color-text-secondary); } .chat-input-modes .chat-mcp-dropdown > .btn .chat-mcp-badge { background: var(--color-primary); color: var(--color-primary-text); } /* 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-modes .chat-mcp-dropdown > .btn:has(.chat-mcp-badge), .chat-input-modes .chat-mcp-dropdown > .btn:has(.chat-mcp-badge):hover { background: var(--color-primary-light); border-color: var(--color-primary-border); color: var(--color-primary); } @media (max-width: 480px) { .chat-mode-chip-label { display: none; } .chat-mode-chip, .chat-input-modes .chat-mcp-dropdown > .btn { padding: 0 8px; } } .chat-input-wrapper:focus-within { border-color: var(--color-primary); box-shadow: 0 0 0 1px var(--color-primary-border); } .chat-attach-btn { flex-shrink: 0; border: none !important; background: transparent !important; color: var(--color-text-muted) !important; padding: var(--spacing-xs) !important; } .chat-attach-btn:hover { color: var(--color-primary) !important; } .chat-input { flex: 1; background: transparent; color: var(--color-text-primary); border: none; padding: 6px var(--spacing-sm); font-size: 0.875rem; font-family: inherit; outline: none; resize: none; min-height: 32px; 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: 32px; height: 32px; 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 { opacity: 0.3; cursor: not-allowed; } .chat-send-btn:active:not(:disabled) { transform: scale(0.92); } .chat-stop-btn { padding: var(--spacing-xs); background: var(--color-error); color: white; border: none; border-radius: var(--radius-md); cursor: pointer; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; flex-shrink: 0; align-self: flex-start; transition: background var(--duration-fast); } .chat-stop-btn:hover { background: var(--color-error-hover, #dc2626); } .chat-token-info { display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-xs) var(--spacing-lg); font-size: 0.75rem; color: var(--color-text-muted); } .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 */ .chat-streaming-speed { font-size: 0.6875rem; color: var(--color-text-muted); padding-top: var(--spacing-xs); font-family: var(--font-mono); display: flex; align-items: center; gap: 4px; } /* Thinking dots animation */ .chat-thinking-indicator { display: flex; align-items: center; min-height: 24px; } .chat-thinking-dots { display: inline-flex; gap: 4px; align-items: center; } .chat-thinking-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--color-text-muted); animation: thinkingBounce 1.4s infinite ease-in-out both; } .chat-thinking-dots span:nth-child(1) { animation-delay: -0.32s; } .chat-thinking-dots span:nth-child(2) { animation-delay: -0.16s; } .chat-thinking-dots span:nth-child(3) { animation-delay: 0s; } @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) */ .chat-staging-progress { display: flex; flex-direction: column; gap: 6px; min-width: 200px; max-width: 320px; } .chat-staging-label { font-size: 0.8rem; color: var(--color-text-secondary); display: flex; align-items: center; gap: 6px; } .chat-staging-label i { color: var(--color-primary); } .chat-staging-detail { display: flex; align-items: center; gap: 8px; } .chat-staging-bar-container { flex: 1; height: 4px; background: var(--color-bg-tertiary); border-radius: 2px; overflow: hidden; } .chat-staging-bar { height: 100%; background: var(--color-primary); border-radius: 2px; transition: width 300ms ease; } .chat-staging-pct { font-size: 0.75rem; color: var(--color-text-muted); min-width: 32px; text-align: right; } .chat-staging-file { font-size: 0.7rem; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Message completion flash — briefly highlights the last assistant bubble when streaming ends */ @keyframes messageCompletionFlash { 0% { box-shadow: 0 0 0 0 var(--color-primary-border); } 40% { box-shadow: 0 0 0 3px var(--color-primary-light); } 100% { box-shadow: 0 0 0 0 transparent; } } .chat-message-new .chat-message-content { animation: messageCompletionFlash 600ms ease-out; } @media (prefers-reduced-motion: reduce) { .chat-message { animation: none; } .chat-message-new .chat-message-content { animation: none; } .chat-thinking-dots span { animation: none; opacity: 0.7; } } /* 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-suggestions { display: flex; flex-wrap: wrap; gap: var(--spacing-sm); justify-content: center; margin-bottom: var(--spacing-lg); max-width: 520px; } .chat-empty-suggestion { padding: 8px var(--spacing-md); background: var(--color-surface-raised); border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); font-size: var(--text-sm); font-weight: var(--font-weight-medium); font-family: inherit; color: var(--color-text-secondary); cursor: pointer; box-shadow: var(--shadow-subtle), var(--shadow-inset-top); transition: all var(--duration-fast); } .chat-empty-suggestion:hover { border-color: var(--color-primary-border); color: var(--color-primary); background: var(--color-surface-raised); transform: translateY(-1px); box-shadow: var(--shadow-sm), var(--shadow-inset-top); } .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 i { font-size: 0.625rem; } /* Recent strip on the empty state — replaces the old persistent conversation sidebar. Visible only while messages.length === 0. */ .chat-recent-strip { display: flex; flex-direction: column; gap: var(--spacing-sm); width: 100%; max-width: 720px; margin-top: var(--spacing-sm); } .chat-recent-strip-label { display: flex; align-items: center; gap: var(--spacing-xs); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-muted); text-align: left; } .chat-recent-strip-kbd { font-family: var(--font-mono); font-size: 0.65rem; padding: 1px 5px; border-radius: var(--radius-sm); background: var(--color-bg-tertiary); color: var(--color-text-secondary); border: 1px solid var(--color-border-subtle); text-transform: none; letter-spacing: 0; } .chat-recent-strip-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--spacing-sm); } .chat-recent-strip-item { display: flex; flex-direction: column; gap: 2px; padding: var(--spacing-sm) var(--spacing-md); background: var(--color-surface-raised); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); font-family: inherit; text-align: left; cursor: pointer; transition: border-color var(--duration-fast), background var(--duration-fast), transform var(--duration-fast); } .chat-recent-strip-item:hover { border-color: var(--color-primary-border); background: var(--color-surface-raised); transform: translateY(-1px); } .chat-recent-strip-item-name { font-size: 0.8125rem; font-weight: 600; color: var(--color-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .chat-recent-strip-item-preview { font-size: 0.75rem; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .chat-recent-strip-item-time { font-size: 0.6875rem; color: var(--color-text-tertiary); margin-top: 2px; } /* ChatsMenu — popover replacing the old persistent history sidebar. */ .chats-menu { position: relative; display: inline-block; flex-shrink: 0; } .chats-menu-trigger { display: inline-flex; align-items: center; gap: var(--spacing-xs); } .chats-menu-trigger.active { background: var(--color-primary-light); border-color: var(--color-primary-border); color: var(--color-primary); } .chats-menu-trigger-label { font-weight: 500; } .chats-menu-trigger-kbd { display: none; font-family: var(--font-mono); font-size: 0.65rem; padding: 1px 5px; border-radius: var(--radius-sm); background: var(--color-bg-tertiary); color: var(--color-text-muted); border: 1px solid var(--color-border-subtle); } @media (min-width: 640px) { .chats-menu-trigger-kbd { display: inline-block; } } .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-bg-primary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); display: flex; flex-direction: column; max-height: min(70vh, 520px); animation: dropdownIn 120ms ease-out; } .chats-menu-search { position: relative; padding: var(--spacing-xs) var(--spacing-sm); border-bottom: 1px solid var(--color-border-divider); } .chats-menu-search-icon { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); color: var(--color-text-muted); font-size: 0.75rem; pointer-events: none; } .chats-menu-search-input { width: 100%; padding: 6px 26px 6px 28px; background: var(--color-bg-secondary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); font-size: 0.8125rem; color: var(--color-text-primary); outline: none; transition: border-color var(--duration-fast); } .chats-menu-search-input:focus { border-color: var(--color-primary-border); } .chats-menu-search-clear { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--color-text-muted); cursor: pointer; font-size: 0.75rem; padding: 2px; } .chats-menu-list { flex: 1; overflow-y: auto; padding: var(--spacing-xs); scrollbar-width: thin; } .chats-menu-empty { padding: var(--spacing-md); text-align: center; font-size: 0.8125rem; color: var(--color-text-muted); } .chats-menu-item { display: flex; align-items: flex-start; gap: var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius-sm); cursor: pointer; font-size: var(--text-sm); color: var(--color-text-secondary); border-left: 2px solid transparent; transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast); } .chats-menu-item.highlighted { background: var(--color-bg-hover); color: var(--color-text-primary); } .chats-menu-item.active { color: var(--color-primary); border-left-color: var(--color-primary); background: var(--color-primary-light); } .chats-menu-item-icon { font-size: 0.7rem; margin-top: 4px; color: var(--color-text-tertiary); flex-shrink: 0; } .chats-menu-item-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .chats-menu-item-top { display: flex; align-items: center; gap: var(--spacing-xs); } .chats-menu-item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; font-size: 0.8125rem; } .chats-menu-item-spin { margin-right: 6px; font-size: 0.7rem; opacity: 0.7; } .chats-menu-item-time { font-size: 0.65rem; color: var(--color-text-muted); white-space: nowrap; flex-shrink: 0; } .chats-menu-item-preview { font-size: 0.7rem; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.3; } .chats-menu-item-rename { flex: 1; font-size: 0.8125rem; padding: 2px 4px; } .chats-menu-item-actions { display: flex; gap: 2px; opacity: 0; transition: opacity var(--duration-fast); flex-shrink: 0; } .chats-menu-item:hover .chats-menu-item-actions, .chats-menu-item.highlighted .chats-menu-item-actions { opacity: 1; } .chats-menu-item-actions button { background: none; border: none; color: var(--color-text-muted); cursor: pointer; padding: 2px 4px; font-size: 0.7rem; border-radius: var(--radius-sm); } .chats-menu-item-actions button:hover { color: var(--color-text-primary); background: var(--color-bg-tertiary); } .chats-menu-item-actions .chats-menu-item-delete:hover { color: var(--color-error); } .chats-menu-footer { display: flex; gap: var(--spacing-xs); padding: var(--spacing-xs); border-top: 1px solid var(--color-border-divider); } .chats-menu-new { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: var(--spacing-xs); } /* 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); } } /* Settings drawer — Manage mode toggle row at the top */ .chat-settings-toggle-row { display: flex; align-items: flex-start; gap: var(--spacing-md); padding: var(--spacing-sm) 0; border-bottom: 1px solid var(--color-border-divider); margin-bottom: var(--spacing-sm); } .chat-settings-toggle-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .chat-settings-toggle-title { font-size: 0.8125rem; font-weight: 600; color: var(--color-text-primary); display: inline-flex; align-items: center; gap: var(--spacing-xs); } .chat-settings-toggle-title i { color: var(--color-accent); font-size: 0.8rem; } .chat-settings-toggle-desc { font-size: 0.75rem; color: var(--color-text-muted); line-height: 1.4; } /* Settings drawer — destructive action area at the bottom */ .chat-settings-danger-zone { margin-top: var(--spacing-md); padding-top: var(--spacing-md); border-top: 1px solid var(--color-border-divider); } .chat-settings-danger-btn { display: inline-flex; align-items: center; gap: var(--spacing-xs); width: 100%; justify-content: center; padding: 8px var(--spacing-md); background: transparent; border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); color: var(--color-error); font-family: inherit; font-size: 0.8125rem; font-weight: 500; cursor: pointer; transition: background var(--duration-fast), border-color var(--duration-fast); } .chat-settings-danger-btn:hover { background: rgba(220, 38, 38, 0.08); border-color: var(--color-error); } /* 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; border-left: 2px solid var(--color-border-subtle); } .chat-activity-streaming { border-left-color: var(--color-primary); } .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 i { 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; border-left: 2px solid transparent; margin-left: -2px; 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-text { font-size: 0.7rem; color: var(--color-text-secondary); word-break: break-word; white-space: pre-wrap; } .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; } .chat-activity-item-code { margin: 2px 0 0; font-size: 0.65rem; white-space: pre-wrap; word-break: break-word; color: var(--color-text-muted); max-height: 120px; overflow-y: auto; } .chat-activity-item-code code { font-family: var(--font-mono); font-size: 0.65rem; } .chat-activity-params { display: flex; flex-direction: column; gap: 3px; margin-top: 2px; } .chat-activity-param { display: flex; gap: 6px; font-size: 0.675rem; line-height: 1.4; word-break: break-word; } .chat-activity-param-key { color: var(--color-text-muted); flex-shrink: 0; opacity: 0.7; } .chat-activity-param-val { color: var(--color-text-secondary); white-space: pre-wrap; word-break: break-word; min-width: 0; } .chat-activity-param-val-long { max-height: 80px; overflow-y: auto; } .chat-activity-thinking { border-left-color: var(--color-info-border); } .chat-activity-tool-call { border-left-color: var(--color-warning-border); } .chat-activity-tool-result { border-left-color: var(--color-success-border); } /* Context window progress bar */ .chat-context-bar { position: relative; height: 18px; background: var(--color-bg-tertiary); border-bottom: 1px solid var(--color-border-subtle); overflow: hidden; } .chat-context-progress { position: absolute; top: 0; left: 0; height: 100%; transition: width 300ms ease; opacity: 0.3; } .chat-context-label { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; font-size: 0.625rem; color: var(--color-text-muted); font-weight: 500; } /* Chat header */ .chat-header { padding: var(--spacing-xs) var(--spacing-md); border-bottom: 1px solid var(--color-border-subtle); display: flex; align-items: center; gap: var(--spacing-xs); background: var(--color-bg-secondary); flex-shrink: 0; min-height: 40px; } .chat-header-title { font-size: 0.8125rem; font-weight: 600; color: var(--color-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; min-width: 0; } .chat-header-shield { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: var(--radius-sm); font-size: 0.75rem; color: var(--color-accent); background: var(--color-accent-light); flex-shrink: 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); } /* Focus mode: once a conversation is underway, fade non-essential header chrome and pull the messages padding tighter. Hover or focus on the header brings everything back; Esc removes focus mode for the rest of the session. */ .chat--focus .chat-header-title, .chat--focus .chat-header-shield { opacity: 0.45; transition: opacity var(--duration-normal) var(--ease-default); } .chat--focus .chat-header:hover .chat-header-title, .chat--focus .chat-header:hover .chat-header-shield, .chat--focus .chat-header:focus-within .chat-header-title, .chat--focus .chat-header:focus-within .chat-header-shield { opacity: 1; } .chat--focus .chat-messages { padding-top: var(--spacing-sm); } @media (prefers-reduced-motion: reduce) { .chat--focus .chat-header-title, .chat--focus .chat-header-shield, .chat-messages { transition: none; } } /* 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-bg-primary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); 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 */ .chat-model-info-panel { background: var(--color-bg-secondary); border-bottom: 1px solid var(--color-border-subtle); animation: fadeIn 150ms ease; } .chat-model-info-header { display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-xs) var(--spacing-md); font-size: 0.8125rem; font-weight: 600; color: var(--color-text-primary); border-bottom: 1px solid var(--color-border-divider); } .chat-model-info-body { padding: var(--spacing-sm) var(--spacing-md); display: flex; flex-direction: column; gap: 4px; max-height: 200px; overflow-y: auto; } .chat-model-info-row { display: flex; align-items: center; justify-content: space-between; font-size: 0.8125rem; padding: 2px 0; } .chat-model-info-row > span:first-child { color: var(--color-text-secondary); font-weight: 500; } .chat-model-info-row > span:last-child { color: var(--color-text-primary); font-family: var(--font-mono); font-size: 0.75rem; max-width: 60%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; } /* Settings drawer */ .chat-settings-overlay { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.3); z-index: 10; opacity: 0; pointer-events: none; transition: opacity 200ms; } .chat-settings-overlay.open { opacity: 1; pointer-events: auto; } .chat-settings-drawer { position: absolute; top: 0; right: 0; bottom: 0; width: 320px; max-width: 90%; background: var(--color-bg-secondary); border-left: 1px solid var(--color-border-subtle); z-index: 11; display: flex; flex-direction: column; transform: translateX(100%); transition: transform 250ms var(--ease-default); box-shadow: var(--shadow-lg); will-change: transform; } .chat-settings-drawer.open { transform: translateX(0); } .chat-settings-drawer-header { display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-md); border-bottom: 1px solid var(--color-border-subtle); font-weight: 600; font-size: 0.875rem; } .chat-settings-drawer-body { flex: 1; overflow-y: auto; padding: var(--spacing-md); display: flex; flex-direction: column; gap: var(--spacing-sm); } /* 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 */ .chat-max-tps-badge { background: rgba(59, 130, 246, 0.15); color: var(--color-primary); padding: 1px 6px; border-radius: var(--radius-sm); font-weight: 600; font-size: 0.7rem; } /* Slider styles */ .chat-slider { width: 100%; height: 4px; appearance: none; -webkit-appearance: none; background: var(--color-border-default); border-radius: 2px; outline: none; } .chat-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--color-primary); cursor: pointer; } .chat-slider::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--color-primary); cursor: pointer; border: none; } .chat-slider-labels { display: flex; justify-content: space-between; font-size: 0.625rem; color: var(--color-text-muted); margin-top: 2px; } /* Message inline files */ .chat-message-files { display: flex; gap: var(--spacing-xs); flex-wrap: wrap; margin-top: var(--spacing-xs); } .chat-file-inline { display: inline-flex; align-items: center; gap: 4px; padding: 2px 6px; background: color-mix(in oklab, currentColor 12%, transparent); border-radius: var(--radius-sm); font-size: 0.7rem; color: var(--color-text-secondary); } .chat-inline-image { max-width: 200px; max-height: 200px; border-radius: var(--radius-md); margin-top: var(--spacing-xs); } .chat-files { display: flex; gap: var(--spacing-xs); flex-wrap: wrap; padding: var(--spacing-xs) var(--spacing-lg); } .chat-file-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; background: var(--color-bg-tertiary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); font-size: 0.75rem; color: var(--color-text-secondary); max-width: 220px; } /* Image attachments preview as a thumbnail instead of a bare icon. */ .chat-file-badge--image { padding: 3px 8px 3px 3px; border-radius: var(--radius-lg); } .chat-file-thumb { width: 28px; height: 28px; border-radius: var(--radius-sm); object-fit: cover; flex-shrink: 0; } .chat-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .chat-file-badge button { background: none; border: none; color: var(--color-text-muted); cursor: pointer; padding: 0; font-size: 0.625rem; } /* Studio tabs */ .studio-tabs { display: flex; flex-wrap: wrap; gap: var(--spacing-xs); border-bottom: 1px solid var(--color-border-subtle); padding: var(--spacing-sm) var(--spacing-xl) 0; background: var(--color-bg-primary); position: sticky; top: 0; z-index: 10; } .studio-tab { display: flex; align-items: center; gap: var(--spacing-xs); background: none; border: none; padding: 10px var(--spacing-md); font-size: var(--text-sm); font-family: inherit; font-weight: var(--font-weight-medium); color: var(--color-text-secondary); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color var(--duration-fast), border-color var(--duration-fast); } .studio-tab:hover { color: var(--color-text-primary); } .studio-tab-active { color: var(--color-primary); border-bottom-color: var(--color-primary); font-weight: var(--font-weight-semibold); } /* Two-column layout for media generation pages */ .media-layout { display: grid; grid-template-columns: minmax(320px, 420px) 1fr; gap: var(--spacing-lg); padding: var(--spacing-xl); max-width: var(--page-max-default); margin: 0 auto; width: 100%; align-items: start; } @media (max-width: 900px) { .media-layout { grid-template-columns: 1fr; } } /* The workbench control column. A hairline field stack rather than a shadowed card of boxed groups: the panel is the page's left half, not an object floating on it, and the same treatment covers all six generators because they share this class. */ .media-controls { background: transparent; border: 0; border-top: 1px solid var(--color-border-default); border-radius: 0; padding: 0; box-shadow: none; position: sticky; top: var(--spacing-lg); } /* Each control is a row on the stack, separated by the same hairline the lanes use, so a form and a list read as the same system. */ .media-controls .form-group { margin-bottom: 0; padding: var(--spacing-sm) 0; border-bottom: 1px solid var(--color-border-default); } /* Uppercase micro-label, matching the eyebrow treatment used elsewhere: it names the field without competing with the value. */ .media-controls .form-label { font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--color-text-muted); } /* The submit row closes the stack, so it carries no divider of its own. */ .media-controls .form-group:last-of-type, .media-controls > form > .btn-full:last-child { border-bottom: 0; } .media-controls .form-grid-2col, .media-controls .form-grid-3col { margin-bottom: var(--spacing-md); } .media-controls .form-grid-2col .form-group, .media-controls .form-grid-3col .form-group { margin-bottom: 0; } .media-controls .page-header { margin-bottom: var(--spacing-lg); padding-bottom: var(--spacing-md); border-bottom: 1px solid var(--color-border-divider); } .media-controls .page-title { font-size: var(--text-lg); font-weight: var(--font-weight-semibold); display: flex; align-items: center; gap: var(--spacing-sm); } .media-controls .page-title i { color: var(--color-accent); } .media-preview { display: flex; flex-direction: column; gap: var(--spacing-md); } .media-result { background: var(--color-bg-secondary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); padding: var(--spacing-lg); min-height: 320px; display: flex; align-items: center; justify-content: center; } .media-result img, .media-result video { max-width: 100%; border-radius: var(--radius-md); } .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); } /* Generation progress: shimmer placeholder tiles + elapsed time. */ .gen-progress { display: flex; flex-direction: column; align-items: center; gap: var(--spacing-lg); width: 100%; } .gen-progress__tiles { display: grid; gap: var(--spacing-sm); width: 100%; max-width: 520px; } .gen-progress__tiles--n1 { grid-template-columns: 1fr; } .gen-progress__tiles--n2, .gen-progress__tiles--n3, .gen-progress__tiles--n4 { grid-template-columns: repeat(2, 1fr); } .gen-progress__tile { width: 100%; aspect-ratio: 1 / 1; height: auto; border-radius: var(--radius-md); } .gen-progress__status { display: inline-flex; align-items: center; gap: var(--spacing-sm); color: var(--color-text-secondary); font-size: var(--text-sm); } .gen-progress__time { color: var(--color-text-muted); } /* Talk voice visualizer: status-tinted frequency bars. */ .voice-viz { display: block; width: 100%; height: 88px; margin-bottom: var(--spacing-md); --viz-color: var(--color-text-muted); transition: opacity var(--duration-normal) var(--ease-default); } .voice-viz--disconnected { --viz-color: var(--color-text-disabled); opacity: 0.55; } .voice-viz--connected { --viz-color: var(--color-primary); } .voice-viz--listening { --viz-color: var(--color-success); } .voice-viz--speaking { --viz-color: var(--color-accent); } .voice-viz--error { --viz-color: var(--color-error); } /* Media generation history */ .media-history { margin-top: var(--spacing-md); } .media-history-clear-btn { background: none; border: none; color: var(--color-text-muted); cursor: pointer; padding: 2px 6px; font-size: 0.75rem; border-radius: var(--radius-sm); transition: color var(--duration-fast); } .media-history-clear-btn:hover { color: var(--color-danger); } .media-history-list { max-height: 400px; overflow-y: auto; padding: var(--spacing-xs) 0; } .media-history-empty { text-align: center; color: var(--color-text-muted); font-size: 0.8125rem; padding: var(--spacing-md); } .media-history-item { display: flex; align-items: flex-start; gap: var(--spacing-xs); padding: var(--spacing-sm); border-radius: var(--radius-md); cursor: pointer; font-size: 0.8125rem; color: var(--color-text-secondary); transition: background var(--duration-fast), transform var(--duration-fast); margin-bottom: 2px; } .media-history-item:hover { background: var(--color-primary-light); transform: translateX(2px); } .media-history-item.active { background: var(--color-primary-light); color: var(--color-primary); } .media-history-item-thumb { width: 32px; height: 32px; flex-shrink: 0; border-radius: var(--radius-sm); overflow: hidden; display: flex; align-items: center; justify-content: center; background: var(--color-bg-tertiary); color: var(--color-text-muted); font-size: 0.75rem; } .media-history-item-thumb img { width: 100%; height: 100%; object-fit: cover; } .media-history-item-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .media-history-item-top { display: flex; align-items: center; gap: var(--spacing-xs); } .media-history-item-prompt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; font-size: 0.8125rem; } .media-history-item-time { font-size: 0.625rem; color: var(--color-text-muted); white-space: nowrap; flex-shrink: 0; } .media-history-item-model { font-size: 0.6875rem; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.3; } .media-history-item-delete { opacity: 0; background: none; border: none; color: var(--color-text-muted); cursor: pointer; padding: 2px; font-size: 0.75rem; transition: opacity var(--duration-fast); } .media-history-item:hover .media-history-item-delete { opacity: 1; } .media-history-item-delete:hover { color: var(--color-danger); } /* ============================================================ 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; } .chat-settings-drawer { width: 100%; max-width: 100%; } .media-layout { grid-template-columns: 1fr; } .media-controls { position: static; } .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; } .sidebar:not(.open) .nav-icon { width: auto; font-size: 1rem; } .sidebar:not(.open) .sidebar-footer { justify-content: center; flex-direction: column; gap: var(--spacing-xs); } .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; margin: 0; padding: 2px; } /* 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: rgba(0, 0, 0, 0.5); z-index: 40; } } /* 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); } .sidebar.collapsed { width: var(--sidebar-width); } .sidebar.open { transform: translateX(0); } .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 .nav-item { justify-content: flex-start; padding: 10px var(--spacing-md); 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: rgba(0, 0, 0, 0.5); z-index: 40; } } /* 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 */ .canvas-panel { position: relative; width: 45%; max-width: 720px; flex-shrink: 0; border-left: 1px solid var(--color-border-subtle); display: flex; flex-direction: column; background: var(--color-bg-primary); overflow: hidden; animation: canvasSlideIn var(--duration-reveal) var(--ease-reveal) 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(--duration-fast) var(--ease-default); } .canvas-resize-handle:hover, .canvas-resize-handle:active { background: var(--color-primary-light); } /* On narrow screens the canvas takes the full width; no edge-drag resize. */ @media (max-width: 1024px) { .canvas-resize-handle { display: none; } .canvas-panel { width: 100% !important; max-width: none !important; } } .canvas-header-actions { display: inline-flex; gap: var(--spacing-xs); } /* Fullscreen: the canvas covers the viewport. */ .canvas-panel--fullscreen { position: fixed; inset: 0; width: 100% !important; max-width: none !important; z-index: 60; border-left: none; } .canvas-panel-header { display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-sm) var(--spacing-md); border-bottom: 1px solid var(--color-border-subtle); gap: var(--spacing-sm); flex-shrink: 0; } .canvas-panel-title { font-weight: 600; font-size: 0.875rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .canvas-panel-tabs { overflow-x: auto; display: flex; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-md); border-bottom: 1px solid var(--color-border-subtle); flex-shrink: 0; scrollbar-width: thin; } .canvas-panel-tab { display: flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--color-border-subtle); background: transparent; color: var(--color-text-secondary); font-size: 0.75rem; cursor: pointer; white-space: nowrap; flex-shrink: 0; transition: all 150ms; } .canvas-panel-tab:hover { border-color: var(--color-border-default); } .canvas-panel-tab.active { background: var(--color-primary-light); border-color: var(--color-primary); color: var(--color-primary); } .canvas-panel-tab span { max-width: 100px; overflow: hidden; text-overflow: ellipsis; } .canvas-panel-toolbar { display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-md); border-bottom: 1px solid var(--color-border-subtle); flex-shrink: 0; } .canvas-toggle-group { display: flex; border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); overflow: hidden; } .canvas-toggle-btn { padding: 2px 10px; font-size: 0.75rem; border: none; background: transparent; color: var(--color-text-secondary); cursor: pointer; transition: all 150ms; } .canvas-toggle-btn.active { background: var(--color-primary); color: var(--color-primary-text); } .canvas-panel-body { flex: 1; overflow: auto; padding: var(--spacing-md); min-height: 0; } .canvas-panel-body pre { margin: 0; font-size: 0.8125rem; } /* 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 { width: 100%; min-height: 600px; height: calc(100vh - 200px); border: none; background: white; border-radius: var(--radius-md); } .canvas-preview-image { max-width: 100%; border-radius: var(--radius-md); } .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: var(--spacing-sm); } .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); } .chat-empty-suggestions { flex-direction: column; align-items: stretch; } } /* 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(--color-text-danger, #e53e3e); 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); backdrop-filter: blur(4px); animation: fadeIn var(--duration-normal) var(--ease-spring); } .confirm-dialog { background: var(--color-bg-secondary); border: 1px solid var(--color-border-strong); border-radius: var(--radius-lg); max-width: 420px; width: 90%; padding: var(--spacing-lg); box-shadow: var(--shadow-md); animation: popIn var(--duration-slow) var(--ease-spring); 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(--color-error); font-size: 1.125rem; } .confirm-dialog-title { font-size: 1rem; font-weight: 600; color: var(--color-text-primary); } .confirm-dialog-body { font-size: 0.875rem; 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); } .confirm-dialog-actions .btn-danger { background: var(--color-error); color: var(--color-text-inverse); border: 1px solid var(--color-error); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), var(--shadow-inset-hi); } .confirm-dialog-actions .btn-danger:hover:not(:disabled) { filter: brightness(1.06); transform: translateY(-1px); } /* Home page */ .home-page { display: flex; flex-direction: column; flex: 1; justify-content: center; gap: var(--space-section); max-width: var(--page-max-medium); margin: 0 auto; padding: var(--space-section) var(--spacing-lg); width: 100%; } .home-header { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--spacing-lg); flex-wrap: wrap; } .home-eyebrow { font-family: var(--font-mono); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.14em; color: var(--color-eyebrow); } .home-greeting { font-family: var(--font-sans); font-size: clamp(2rem, 1.4rem + 3vw, var(--text-4xl)); font-weight: var(--font-weight-semibold); letter-spacing: -0.02em; line-height: 1.05; margin: var(--spacing-xs) 0 0; } .home-status-line { display: flex; align-items: center; gap: var(--spacing-md); flex-wrap: wrap; } .home-hero { text-align: center; padding: var(--spacing-sm) 0 var(--spacing-md); } .home-logo { width: 72px; height: auto; margin: 0 auto; display: block; } /* Home resource bar - prominent */ .home-resource-bar { width: 100%; max-width: 420px; padding: var(--spacing-md) var(--spacing-lg); background: var(--color-surface-raised); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-subtle); } .home-resource-bar-header { display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--text-sm); color: var(--color-text-secondary); margin-bottom: var(--spacing-sm); } .home-resource-label { font-weight: var(--font-weight-medium); color: var(--color-text-primary); } .home-resource-pct { margin-left: auto; font-family: var(--font-mono); font-weight: var(--font-weight-medium); font-size: var(--text-xs); } .home-resource-track { width: 100%; height: 6px; background: var(--color-surface-sunken); border-radius: var(--radius-full); overflow: hidden; } .home-resource-fill { height: 100%; border-radius: var(--radius-full); transition: width 500ms ease; } .home-cluster-status { display: flex; align-items: center; gap: var(--spacing-xs); font-size: var(--text-xs); color: var(--color-text-muted); margin-top: var(--spacing-sm); } .home-cluster-dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: var(--color-success); display: inline-block; } /* Home assistant CTA — a self-explanatory entry point for the in-process admin tool surface. Distinct from the chat composer below it; uses the accent token + a subtle gradient so it reads as a primary action without looking AI-slop generative. */ .home-assistant-card { display: flex; align-items: center; gap: var(--spacing-md); width: 100%; margin-bottom: var(--spacing-md); padding: var(--spacing-md) var(--spacing-lg); background: var(--color-surface-raised); border: 1px solid var(--color-accent); border-radius: var(--radius-xl); cursor: pointer; text-align: left; font: inherit; color: var(--color-text); transition: background-color 120ms ease, transform 120ms ease, box-shadow 120ms ease; } .home-assistant-card:hover { background: var(--color-accent-light, var(--color-surface-hover)); transform: translateY(-1px); box-shadow: var(--shadow-md); } .home-assistant-card:active { transform: translateY(0); } .home-assistant-icon { flex: 0 0 auto; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: var(--color-accent); color: var(--color-on-accent, #ffffff); font-size: 1.1rem; } .home-assistant-text { flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; min-width: 0; } .home-assistant-title { font-weight: 600; font-size: 1rem; } .home-assistant-desc { font-size: 0.8125rem; color: var(--color-text-muted); } .home-assistant-cta { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; font-size: 0.8125rem; font-weight: 500; color: var(--color-accent); white-space: nowrap; } @media (max-width: 600px) { .home-assistant-card { flex-wrap: wrap; } .home-assistant-cta { flex-basis: 100%; justify-content: flex-end; } } /* Home chat card */ .home-chat-card { width: 100%; background: var(--color-surface-raised); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-xl); padding: var(--spacing-lg); box-shadow: var(--shadow-md); } .home-model-row { display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-md); } .home-file-tags { display: flex; flex-wrap: wrap; gap: var(--spacing-xs); margin-bottom: var(--spacing-sm); } .home-file-tag { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--color-surface-sunken); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); font-size: var(--text-xs); color: var(--color-text-secondary); } .home-file-tag button { background: none; border: none; color: var(--color-text-muted); cursor: pointer; padding: 0; font-size: 0.625rem; } .home-file-tag button:hover { color: var(--color-error); } /* Home input container */ .home-input-container { background: var(--color-surface-sunken); border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); transition: border-color var(--duration-fast), box-shadow var(--duration-fast); } .home-input-container:focus-within { border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-light); } .home-textarea { width: 100%; background: transparent; color: var(--color-text-primary); border: none; border-radius: var(--radius-lg) var(--radius-lg) 0 0; padding: var(--spacing-md); font-size: var(--text-base); font-family: inherit; outline: none; resize: none; min-height: 84px; line-height: var(--leading-normal); } .home-textarea::placeholder { color: var(--color-text-muted); } .home-input-footer { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm) var(--spacing-xs) var(--spacing-md); border-top: 1px solid var(--color-border-divider); } .home-attach-buttons { display: flex; gap: 2px; } .home-attach-btn { background: none; border: none; color: var(--color-text-muted); cursor: pointer; padding: 6px 8px; font-size: var(--text-sm); border-radius: var(--radius-md); transition: color var(--duration-fast), background var(--duration-fast); } .home-attach-btn:hover { color: var(--color-primary); background: var(--color-primary-light); } .home-input-hint { flex: 1; text-align: center; font-size: var(--text-xs); color: var(--color-text-muted); letter-spacing: 0.02em; } .home-send-btn { display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; background: var(--color-primary); color: var(--color-primary-text); border: none; border-radius: var(--radius-full); font-size: var(--text-sm); cursor: pointer; box-shadow: var(--shadow-sm); transition: background var(--duration-fast), transform 100ms, box-shadow var(--duration-fast); flex-shrink: 0; } .home-send-btn:hover:not(:disabled) { background: var(--color-primary-hover); transform: scale(1.05); } .home-send-btn:disabled { opacity: 0.3; cursor: not-allowed; } .home-send-btn:active:not(:disabled) { transform: scale(0.92); } /* Home quick links */ .home-quick-links { display: flex; flex-wrap: wrap; gap: var(--spacing-sm); justify-content: center; } .home-link-btn { display: inline-flex; align-items: center; gap: var(--spacing-xs); padding: 8px var(--spacing-md); background: var(--color-surface-raised); color: var(--color-text-secondary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-weight-medium); font-family: inherit; cursor: pointer; text-decoration: none; box-shadow: var(--shadow-subtle); transition: all var(--duration-fast); } .home-link-btn:hover { border-color: var(--color-primary-border); color: var(--color-primary); transform: translateY(-1px); box-shadow: var(--shadow-sm); } /* Quiet variant: a deliberately understated link (e.g. Documentation) so the quick-links row keeps a single clear primary action. */ .home-link-btn--quiet { opacity: 0.8; } /* Home loaded models */ .home-loaded { width: 100%; display: flex; flex-direction: column; gap: var(--spacing-sm); } .home-loaded-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--spacing-xs); } .home-loaded-item { display: inline-flex; align-items: center; gap: 6px; padding: 3px 6px 3px 10px; background: var(--color-surface-sunken); border: 1px solid var(--color-border-divider); border-radius: var(--radius-sm); font-size: var(--text-xs); } .home-loaded-item button { background: none; border: none; color: var(--color-error); cursor: pointer; padding: 0; line-height: 1; font-size: 0.625rem; } .home-loaded-empty { color: var(--color-text-secondary); font-size: var(--text-sm); margin: 0; } .home-stop-all { align-self: flex-start; } /* Home wizard (no models) */ .home-wizard { max-width: 48rem; width: 100%; /* .home-page is a stretch column flex; a max-width child would otherwise pin to the left cross-start edge. Center it. */ margin: 0 auto; } .home-wizard-hero { text-align: center; padding: var(--spacing-xl) 0; } .home-wizard-hero h1 { font-size: var(--text-2xl); font-weight: var(--font-weight-semibold); letter-spacing: -0.015em; margin-bottom: var(--spacing-sm); color: var(--color-text-primary); } .home-wizard-hero p { color: var(--color-text-secondary); font-size: var(--text-base); line-height: var(--leading-normal); } .home-wizard-steps { margin-bottom: var(--spacing-xl); } .home-wizard-steps h2 { font-size: var(--text-lg); font-weight: var(--font-weight-semibold); margin-bottom: var(--spacing-md); } .home-wizard-step { display: flex; gap: var(--spacing-md); align-items: flex-start; padding: var(--spacing-sm) 0; } .home-wizard-step-num { width: 28px; height: 28px; border-radius: 50%; background: var(--color-primary); color: white; display: flex; align-items: center; justify-content: center; font-size: 0.8125rem; font-weight: 600; flex-shrink: 0; } .home-wizard-step strong { display: block; margin-bottom: 2px; } .home-wizard-step p { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; } .home-wizard-actions { display: flex; gap: var(--spacing-sm); justify-content: center; } /* ──────────────────── Home: hardware-aware starter models ──────────────────── */ .home-starters { margin: var(--spacing-lg) 0; padding: var(--spacing-lg); } .home-starters-head { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-md); } .home-starters-head strong { font-size: 0.9375rem; } .home-starters-tier { display: inline-flex; align-items: center; gap: var(--spacing-xs); font-size: 0.75rem; color: var(--color-text-muted); } .home-starters-sub { margin: var(--spacing-xs) 0 var(--spacing-md); font-size: 0.8125rem; color: var(--color-text-secondary); } .home-starters-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--spacing-xs); } .home-starters-item { display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-xs) 0; } .home-starters-name { font-weight: 500; font-size: 0.875rem; word-break: break-all; } .home-starters-badge { font-size: 0.625rem; } .home-starters-size { margin-left: auto; font-size: 0.75rem; color: var(--color-text-muted); white-space: nowrap; } /* ──────────────────── 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; } /* Collapsed keeps the strip and its heading, only dropping the cards, so the panel is recoverable in place rather than gone. Tighter block padding is what actually returns vertical space to the gallery. */ /* The class sets display:grid, which would otherwise beat the UA [hidden] rule. */ .rec-models-grid[hidden] { display: none; } @keyframes rec-models-reveal { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } } @media (prefers-reduced-motion: reduce) { .rec-models-grid { animation: none; } } .rec-models-item { display: flex; flex-direction: column; gap: var(--spacing-xs); padding: var(--spacing-sm) var(--spacing-md); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); background: var(--color-bg-primary); } .rec-models-item-name { font-weight: 500; font-size: 0.8125rem; word-break: break-all; } .rec-models-item-meta { display: flex; gap: var(--spacing-sm); font-size: 0.75rem; color: var(--color-text-muted); } .rec-models-item-fit { display: inline-flex; align-items: center; gap: 4px; } /* ──────────────────── Home: drop-in endpoint / API compatibility ──────────────────── */ .home-connect { max-width: 760px; margin: var(--spacing-xl) auto 0; padding: var(--spacing-lg); } .home-connect-head { display: flex; align-items: flex-start; gap: var(--spacing-md); } .home-connect-icon { flex-shrink: 0; display: grid; place-items: center; width: 38px; height: 38px; border-radius: var(--radius-lg); background: var(--color-primary-light); color: var(--color-primary); } .home-connect-title { margin: 0; font-size: var(--text-lg); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); } .home-connect-sub { margin: 2px 0 0; font-size: var(--text-sm); color: var(--color-text-muted); line-height: 1.5; } .home-connect-url { display: flex; align-items: center; gap: var(--spacing-sm); margin-top: var(--spacing-md); padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-sm) var(--spacing-md); background: var(--color-surface-sunken); border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); } .home-connect-url code { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; font-size: var(--text-sm); color: var(--color-primary); background: none; padding: 0; } .home-connect-url .btn { flex-shrink: 0; display: inline-flex; align-items: center; gap: 6px; } .home-connect-dismiss { margin-left: auto; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; background: none; border: none; border-radius: var(--radius-md); color: var(--color-text-muted); cursor: pointer; transition: background var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default); } .home-connect-dismiss:hover { background: var(--color-surface-hover); color: var(--color-text-primary); } .home-connect-toggle { display: inline-flex; align-items: center; gap: var(--spacing-xs); margin-top: var(--spacing-md); padding: var(--spacing-xs) 0; background: none; border: none; color: var(--color-primary); font: inherit; font-size: var(--text-sm); font-weight: var(--font-weight-medium); cursor: pointer; } .home-connect-toggle:hover { color: var(--color-primary-hover); } .home-connect-toggle i { font-size: 0.7em; } .home-connect-endpoints { margin-top: var(--spacing-sm); } .home-connect-block { margin-top: var(--spacing-md); } .home-connect-block-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); } .home-connect-block-title { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; font-weight: var(--font-weight-semibold); color: var(--color-text-tertiary); } .home-connect-apis { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-sm); } .home-connect-api { display: flex; align-items: baseline; justify-content: space-between; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-tertiary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); } .home-connect-api-name { font-weight: var(--font-weight-medium); color: var(--color-text-secondary); } .home-connect-api-path { font-size: var(--text-xs); color: var(--color-text-muted); background: none; padding: 0; } .home-connect-docs { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; font-size: var(--text-sm); font-weight: var(--font-weight-medium); color: var(--color-primary); text-decoration: none; } .home-connect-docs:hover { color: var(--color-primary-hover); } .home-connect-docs i { font-size: 0.75rem; transition: transform var(--duration-fast) var(--ease-out, ease-out); } .home-connect-docs:hover i { transform: translateX(3px); } @media (max-width: 560px) { .home-connect-apis { grid-template-columns: 1fr; } } /* ──────────────────── 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 i { 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 i { 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 i { 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 > i { 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 i { color: var(--color-text-muted); } .biometrics-mediainput__meter.recording { border-color: var(--color-error-border); color: var(--color-text-primary); } .biometrics-mediainput__meter.recording i { 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 > i { 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: #fff; 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), 0 0 12px rgba(232, 168, 124, 0.35); 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 i { 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 i { 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: rgba(232, 168, 124, 0.16); 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: rgba(136, 192, 208, 0.16); 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-left: 3px solid var(--color-info); border-radius: var(--radius-md); font-size: var(--text-sm); margin: 0; } .audio-transform-echo__notice > i { 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 > i { 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 i { transition: transform var(--duration-fast); } .biometrics-response[open] summary i { 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 > i { 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 i { 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-sm); background: transparent; color: var(--color-text-muted); cursor: pointer; font-size: var(--text-sm); opacity: 0.45; transition: opacity var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default), background var(--duration-fast) var(--ease-default), border-color var(--duration-fast) var(--ease-default); } .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-tertiary); border-color: var(--color-border-subtle); 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); } .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: 8px 10px; border: 0; border-radius: var(--radius-sm); 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 var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default); } .action-menu__item.is-active, .action-menu__item:hover:not(:disabled) { background: var(--color-bg-tertiary); } .action-menu__item:disabled { cursor: not-allowed; opacity: 0.45; } .action-menu__icon { width: 14px; 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(--color-error); } .action-menu__item.is-danger .action-menu__icon { color: var(--color-error); } .action-menu__item.is-danger:hover:not(:disabled), .action-menu__item.is-danger.is-active { background: var(--color-error-light); color: var(--color-error); } .action-menu__item.is-danger:hover:not(:disabled) .action-menu__icon, .action-menu__item.is-danger.is-active .action-menu__icon { color: var(--color-error); } .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 > i { color: var(--color-text-muted); font-size: var(--text-xs); width: 14px; text-align: center; } /* Pulse modifier — subtle ring on the Pin button when a model is pinned, so pinning is visible at a glance without a second icon next to the name. */ .btn--pulse { animation: btnPulseWarning 1.6s ease-in-out infinite; } @keyframes btnPulseWarning { 0%, 100% { box-shadow: 0 0 0 0 rgba(235, 203, 139, 0); } 50% { box-shadow: 0 0 0 4px rgba(235, 203, 139, 0.18); } } /* 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; } /* Manage summary marker — same .stat-cards layout. Top margin separates the cards from the System Resources card above (otherwise they sit too close to the RAM bar) and bottom margin tightens the gap to the tabs below. */ /* Two class names, so this beats the shared .stat-strip shorthand regardless of which rule the file happens to declare later. The single-class version was silently losing its top margin to `margin: 0 0 …`, leaving the strip flush against the resources card above it. */ .stat-strip.manage-summary { margin-top: var(--spacing-xl); margin-bottom: var(--spacing-lg); /* .page--app is a flex column whose split view takes flex:1, so a child with no intrinsic minimum gets shrunk to nothing. The old cards survived only because .stat-card carried min-height:96px; the figure cells do not, so the strip has to decline to shrink. */ flex: none; } /* 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; } } /* Admin console layout (pathless route) - structural only */ /* 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; } } /* Console layout: a secondary rail panel (Build, Operate) beside the page body. */ .console-layout { display: flex; gap: var(--spacing-lg); align-items: flex-start; padding: var(--spacing-md); } .console-rail { flex: 0 0 220px; position: sticky; top: var(--spacing-md); 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-xl); box-shadow: var(--shadow-subtle); } /* Entrance only when entering the console (not on item-to-item sub-nav). */ .console-rail.console-rail--enter { animation: console-rail-in var(--duration-normal) var(--ease-out, ease-out) both; } @keyframes console-rail-in { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: translateX(0); } } .console-rail-header { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-xs); font-size: var(--text-sm); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); } .console-rail-header__title { display: inline-flex; align-items: center; gap: var(--spacing-sm); } .console-rail-header__title i { color: var(--color-primary); font-size: 0.9rem; } .console-rail-toggle { display: none; width: 34px; height: 34px; place-items: center; border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); background: var(--color-surface-elevated); color: var(--color-text-secondary); cursor: pointer; } .console-rail-toggle:hover { border-color: var(--color-border-strong); color: var(--color-text-primary); } .console-rail-groups { display: flex; flex-direction: column; gap: var(--spacing-xs); } .console-group { display: flex; flex-direction: column; gap: 1px; } .console-group + .console-group { margin-top: var(--spacing-xs); padding-top: var(--spacing-xs); border-top: 1px solid var(--color-border-divider); } .console-group-title { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; font-weight: var(--font-weight-semibold); color: var(--color-text-tertiary); padding: var(--spacing-xs) var(--spacing-sm); } .console-rail .nav-item { margin: 0; padding: 7px var(--spacing-sm); border-radius: var(--radius-md); font-size: var(--text-sm); transition: color var(--duration-fast) var(--ease-out, ease-out), background var(--duration-fast) var(--ease-out, ease-out), transform var(--duration-fast) var(--ease-out, ease-out); } .console-rail .nav-item:hover:not(.active) { transform: translateX(2px); } .console-rail .nav-item.active { box-shadow: none; } .console-rail .nav-item.active .nav-icon { color: var(--color-primary); } .console-body { flex: 1 1 auto; min-width: 0; } @media (max-width: 768px) { .console-layout { flex-direction: column; padding: var(--spacing-sm); } .console-rail { position: static; flex-basis: auto; width: 100%; } .console-rail-toggle { display: grid; } .console-rail-groups { display: none; } /* Thirteen destinations stacked in one column is taller than a phone, so opening the menu used to push the page's own heading past the fold: the menu replaced the page rather than annotating it. Bound it and let it scroll, so the content behind stays reachable. */ .console-rail--open .console-rail-groups { display: flex; max-height: 55vh; overflow-y: auto; overscroll-behavior: contain; } } @media (prefers-reduced-motion: reduce) { .console-rail.console-rail--enter { animation: none; } .console-rail .nav-item:hover:not(.active) { transform: none; } } .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; } .voice-library-page, .voice-create-page { width: 100%; max-width: var(--page-max-default); margin: 0 auto; padding: var(--spacing-md) var(--spacing-md) var(--spacing-xl); animation: fadeIn var(--duration-normal) var(--ease-default); } .voice-library-page .page-header, .voice-create-page .page-header { margin-bottom: var(--spacing-lg); } .voice-library-page .page-title i, .voice-create-page .page-title i { color: var(--color-primary); margin-right: var(--spacing-xs); } .voice-library-summary { display: flex; align-items: center; gap: var(--spacing-md); min-height: 36px; margin: calc(-1 * var(--spacing-sm)) 0 var(--spacing-md); color: var(--color-text-muted); font-size: var(--text-sm); } .voice-library-summary strong { color: var(--color-text-primary); font-family: var(--font-mono); font-size: var(--text-base); } .voice-library-summary__divider { width: 1px; height: 18px; background: var(--color-border-default); } .voice-library-error { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-md); margin-bottom: var(--spacing-md); } .voice-library-shell { display: grid; grid-template-columns: minmax(300px, 0.78fr) minmax(430px, 1.35fr); min-height: 620px; overflow: hidden; background: var(--color-surface-raised); border: 1px solid var(--color-border-default); border-radius: var(--radius-xl); box-shadow: var(--shadow-sm), var(--shadow-inset-top); } .voice-library-master { min-width: 0; border-right: 1px solid var(--color-border-divider); background: var(--color-bg-secondary); } .voice-library-toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--spacing-sm); padding: var(--spacing-md); border-bottom: 1px solid var(--color-border-divider); } .voice-library-search { position: relative; display: flex; align-items: center; min-width: 0; } .voice-library-search i { position: absolute; left: 12px; color: var(--color-text-tertiary); font-size: var(--text-xs); pointer-events: none; } .voice-library-search input { width: 100%; height: 38px; padding: 0 12px 0 34px; color: var(--color-text-primary); background: var(--color-surface-sunken); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); font: inherit; font-size: var(--text-sm); } .voice-library-search input::placeholder { color: var(--color-text-tertiary); } .voice-library-language { width: 138px; height: 38px; font-size: var(--text-sm); } .voice-library-list { display: flex; flex-direction: column; max-height: min(70vh, 760px); overflow-y: auto; overscroll-behavior: contain; } .voice-library-loading { min-height: 260px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--spacing-sm); color: var(--color-text-muted); font-size: var(--text-sm); } /* The empty PANEL, not the button inside it. This was attached to the action for a while, which made the button a 430px transparent box that pushed itself out of view. */ .voice-library-empty { min-height: 430px; border: 0; background: transparent; /* Keep the action inside the panel it belongs to. */ display: flex; flex-direction: column; justify-content: center; } .voice-row { appearance: none; width: 100%; display: grid; grid-template-columns: 38px minmax(0, 1fr) 14px; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-md); color: var(--color-text-primary); text-align: left; background: transparent; border: 0; border-bottom: 1px solid var(--color-border-divider); cursor: pointer; transition: background var(--duration-fast) var(--ease-default), box-shadow var(--duration-fast) var(--ease-default); } .voice-row:hover { background: var(--color-surface-hover); } .voice-row--selected { background: var(--color-primary-light); box-shadow: inset 2px 0 0 var(--color-primary); } .voice-row--selected:hover { background: var(--color-primary-light); } .voice-row__avatar { width: 38px; height: 38px; display: grid; place-items: center; border-radius: var(--radius-full); background: var(--color-surface-elevated); border: 1px solid var(--color-border-default); color: var(--color-primary); font-family: var(--font-mono); font-size: 0.65rem; font-weight: var(--font-weight-semibold); letter-spacing: 0.05em; } .voice-row--selected .voice-row__avatar { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-text); } .voice-row__content { min-width: 0; display: flex; flex-direction: column; gap: 5px; } .voice-row__head { min-width: 0; display: flex; align-items: baseline; justify-content: space-between; gap: var(--spacing-sm); } .voice-row__head strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-sm); } .voice-row__head > span { flex-shrink: 0; color: var(--color-text-tertiary); font-family: var(--font-mono); font-size: 0.625rem; } .voice-row__waveform { height: 22px; display: flex; align-items: center; gap: 2px; overflow: hidden; } .voice-row__waveform > span { flex: 1 1 auto; max-width: 3px; min-width: 1px; border-radius: 1px; background: var(--color-border-strong); } .voice-row--selected .voice-row__waveform > span { background: var(--color-primary); opacity: 0.72; } .voice-row__meta { display: flex; align-items: center; gap: 5px; color: var(--color-text-tertiary); font-size: 0.65rem; } .voice-row__chevron { color: var(--color-text-tertiary); font-size: 0.65rem; } .voice-row--selected .voice-row__chevron { color: var(--color-primary); } .voice-library-detail { min-width: 0; padding: clamp(var(--spacing-lg), 3vw, var(--spacing-xl)); display: flex; flex-direction: column; background: var(--color-surface-raised); } .voice-library-detail > .empty-state { flex: 1; min-height: 420px; } .voice-detail__header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--spacing-lg); margin-bottom: var(--spacing-lg); } .voice-detail__eyebrow { display: block; margin-bottom: var(--spacing-xs); color: var(--color-eyebrow); font-family: var(--font-mono); font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.16em; } .voice-detail__header h2 { margin: 0; font-size: clamp(var(--text-xl), 2.5vw, var(--text-2xl)); line-height: var(--leading-tight); } .voice-detail__header p { margin: var(--spacing-xs) 0 0; max-width: 54ch; color: var(--color-text-secondary); line-height: var(--leading-normal); } .voice-detail__player { padding: var(--spacing-md); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); background: var(--color-surface-sunken); } .voice-detail__section { margin-top: var(--spacing-lg); } .voice-detail__section h3 { margin: 0 0 var(--spacing-sm); color: var(--color-text-secondary); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; } .voice-detail__section blockquote { margin: 0; padding: var(--spacing-md) var(--spacing-lg); border-left: 2px solid var(--color-primary); background: var(--color-bg-tertiary); color: var(--color-text-secondary); font-size: var(--text-base); line-height: var(--leading-relaxed); } .voice-detail__metadata { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin: var(--spacing-lg) 0 0; overflow: hidden; border: 1px solid var(--color-border-divider); border-radius: var(--radius-md); background: var(--color-border-divider); } .voice-detail__metadata > div { min-width: 0; padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-secondary); } .voice-detail__metadata dt { color: var(--color-text-tertiary); font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.06em; } .voice-detail__metadata dd { margin: 3px 0 0; overflow: hidden; text-overflow: ellipsis; color: var(--color-text-primary); font-size: var(--text-sm); } .voice-detail__consent { display: flex; align-items: center; gap: var(--spacing-sm); margin-top: var(--spacing-lg); padding: var(--spacing-sm) var(--spacing-md); color: var(--color-success); background: var(--color-success-light); border: 1px solid var(--color-success-border); border-radius: var(--radius-md); } .voice-detail__consent > i { width: 20px; text-align: center; } .voice-detail__consent > div { display: flex; flex-direction: column; } .voice-detail__consent strong { font-size: var(--text-sm); } .voice-detail__consent span { color: var(--color-text-secondary); font-size: var(--text-xs); } .voice-detail__model-setup { margin-top: var(--spacing-lg); padding: var(--spacing-md); border: 1px solid var(--color-warning-border); border-radius: var(--radius-lg); background: var(--color-warning-light); } .voice-detail__model-setup-heading { display: flex; align-items: flex-start; gap: var(--spacing-sm); } .voice-detail__model-setup-heading > i { width: 24px; padding-top: 2px; color: var(--color-warning); text-align: center; } .voice-detail__model-setup-heading h3 { margin: 0; font-size: var(--text-sm); } .voice-detail__model-setup-heading p { margin: 3px 0 0; color: var(--color-text-secondary); font-size: var(--text-xs); line-height: var(--leading-normal); } .voice-detail__model-loading { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-md) 0 var(--spacing-xs) 32px; color: var(--color-text-secondary); font-size: var(--text-xs); } .voice-detail__model-list { display: flex; flex-direction: column; gap: var(--spacing-xs); margin: var(--spacing-md) 0 0 32px; padding: 0; list-style: none; } .voice-detail__model-list li { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--color-surface-raised); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); } .voice-detail__model-list li > span { min-width: 0; display: flex; flex-direction: column; } .voice-detail__model-list strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); } .voice-detail__model-list small { color: var(--color-text-tertiary); font-family: var(--font-mono); font-size: 0.625rem; } .voice-detail__model-list .btn { flex-shrink: 0; } .voice-detail__model-fallback { margin: var(--spacing-md) 0 0 32px; color: var(--color-text-secondary); font-size: var(--text-xs); } .voice-detail__model-fallback a { color: var(--color-primary); font-weight: var(--font-weight-semibold); } .voice-detail__capability-note { margin: var(--spacing-sm) 0 0 32px; color: var(--color-text-tertiary); font-size: 0.625rem; line-height: var(--leading-normal); } .voice-detail__capability-note i { color: var(--color-success); } .voice-detail__api { margin-top: var(--spacing-lg); overflow: hidden; border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); background: var(--color-surface-sunken); } .voice-detail__api > summary { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-md); padding: var(--spacing-md); cursor: pointer; list-style: none; } .voice-detail__api > summary::-webkit-details-marker { display: none; } .voice-detail__api > summary > span { display: flex; flex-direction: column; } .voice-detail__api > summary strong { font-size: var(--text-sm); } .voice-detail__api > summary small { margin-top: 2px; color: var(--color-text-tertiary); font-size: var(--text-xs); } .voice-detail__api > summary > i { color: var(--color-text-tertiary); font-size: var(--text-xs); transition: transform var(--duration-fast) var(--ease-default); } .voice-detail__api[open] > summary > i { transform: rotate(180deg); } .voice-detail__api-body { padding: var(--spacing-md); border-top: 1px solid var(--color-border-divider); background: var(--color-surface-raised); } .voice-detail__api-body > p { margin: 0; color: var(--color-text-secondary); font-size: var(--text-xs); line-height: var(--leading-normal); } .voice-detail__compatible-models { display: flex; flex-direction: column; gap: var(--spacing-xs); margin-top: var(--spacing-md); } .voice-detail__compatible-models > strong { color: var(--color-text-secondary); font-size: var(--text-xs); } .voice-detail__compatible-models > span { color: var(--color-text-tertiary); font-size: var(--text-xs); } .voice-detail__compatible-models ul { display: flex; flex-wrap: wrap; gap: var(--spacing-xs); margin: 0; padding: 0; list-style: none; } .voice-detail__compatible-models li { display: inline-flex; align-items: center; gap: var(--spacing-xs); padding: 4px 8px; background: var(--color-success-light); border: 1px solid var(--color-success-border); border-radius: var(--radius-full); font-size: var(--text-xs); } .voice-detail__compatible-models li small { color: var(--color-text-tertiary); font-family: var(--font-mono); font-size: 0.6rem; } .voice-detail__code-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-sm); margin-top: var(--spacing-md); color: var(--color-text-secondary); font-size: var(--text-xs); font-weight: var(--font-weight-semibold); } .voice-detail__api pre { max-width: 100%; margin: var(--spacing-xs) 0 0; padding: var(--spacing-md); overflow-x: auto; background: var(--color-bg-primary); border: 1px solid var(--color-border-divider); border-radius: var(--radius-md); color: var(--color-text-primary); font-size: 0.6875rem; line-height: var(--leading-relaxed); } .voice-detail__api code { font-family: var(--font-mono); } .voice-detail__api-body > .voice-detail__api-note { margin-top: var(--spacing-sm); color: var(--color-text-tertiary); } .voice-detail__actions { display: flex; align-items: center; gap: var(--spacing-sm); margin-top: auto; padding-top: var(--spacing-xl); } .voice-detail__actions .btn-danger { margin-left: auto; } /* Compact profile picker inside the existing TTS control column. */ .tts-voice-label-row { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-sm); } .tts-voice-label-row .badge { margin-bottom: var(--spacing-xs); } .tts-voice-picker { display: flex; flex-direction: column; gap: var(--spacing-xs); } .tts-voice-picker__selection { display: grid; grid-template-columns: 30px minmax(0, 1fr) auto; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); color: var(--color-text-primary); background: var(--color-primary-light); border: 1px solid var(--color-primary-border); border-radius: var(--radius-md); } .tts-voice-picker__avatar { width: 30px; height: 30px; display: grid; place-items: center; color: var(--color-primary-text); background: var(--color-primary); border-radius: var(--radius-full); font-family: var(--font-mono); font-size: 0.58rem; font-weight: var(--font-weight-semibold); } .tts-voice-picker__selection > span:nth-child(2) { min-width: 0; display: flex; flex-direction: column; } .tts-voice-picker__selection strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-sm); } .tts-voice-picker__selection small { color: var(--color-text-secondary); font-size: var(--text-xs); } .tts-voice-picker__selection > i { color: var(--color-primary); } .tts-voice-picker__hint, .tts-voice-picker__error { margin: 0; color: var(--color-text-muted); font-size: var(--text-xs); line-height: var(--leading-normal); } .tts-voice-picker__error { color: var(--color-error); } .tts-voice-picker__hint a, .tts-voice-picker__manage { color: var(--color-primary); font-weight: var(--font-weight-medium); text-decoration: none; } .tts-voice-picker__manage { display: inline-flex; align-items: center; gap: 5px; width: fit-content; font-size: var(--text-xs); } .tts-voice-picker__manage i { font-size: 0.65em; transition: transform var(--duration-fast) var(--ease-default); } .tts-voice-picker__manage:hover i { transform: translateX(2px); } /* Full-page creation flow: the content form and a quiet sticky readiness rail. */ .voice-create-page { max-width: 1180px; padding-top: var(--spacing-xl); } .voice-create-grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: var(--spacing-lg); align-items: start; } .voice-create-form { overflow: hidden; background: var(--color-surface-raised); border: 1px solid var(--color-border-default); border-radius: var(--radius-xl); box-shadow: var(--shadow-sm), var(--shadow-inset-top); } .voice-create-section { padding: var(--spacing-xl); border-bottom: 1px solid var(--color-border-divider); } .voice-create-section__heading { display: grid; grid-template-columns: 32px minmax(0, 1fr); gap: var(--spacing-md); margin-bottom: var(--spacing-lg); } .voice-create-section__heading > span { padding-top: 3px; color: var(--color-primary); font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: 0.08em; } .voice-create-section__heading h2 { margin: 0; color: var(--color-text-primary); font-size: var(--text-lg); } .voice-create-section__heading p { margin: 4px 0 0; max-width: 62ch; color: var(--color-text-muted); font-size: var(--text-sm); line-height: var(--leading-normal); } .voice-create-processing { display: flex; align-items: center; gap: var(--spacing-sm); margin-top: var(--spacing-sm); color: var(--color-text-secondary); font-size: var(--text-sm); } .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); } .voice-create-preview { margin-top: var(--spacing-md); padding: var(--spacing-md); background: var(--color-surface-sunken); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); } .voice-create-preview__meta { display: flex; flex-wrap: wrap; gap: var(--spacing-xs) var(--spacing-md); margin-top: var(--spacing-sm); color: var(--color-text-muted); font-size: var(--text-xs); } .voice-create-preview__meta span { display: inline-flex; align-items: center; gap: 5px; } .voice-create-preview__meta .tone-success { color: var(--color-success); } .voice-create-preview__meta .tone-warning { color: var(--color-warning); } .voice-create-label-row { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-md); } .voice-create-label-row > span { color: var(--color-text-tertiary); font-family: var(--font-mono); font-size: 0.625rem; } .voice-consent-check { position: relative; display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: var(--spacing-md); align-items: start; padding: var(--spacing-md); background: var(--color-bg-tertiary); border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); cursor: pointer; } .voice-consent-check:hover { border-color: var(--color-border-strong); } .voice-consent-check > input { position: absolute; opacity: 0; pointer-events: none; } .voice-consent-check__box { width: 22px; height: 22px; display: grid; place-items: center; color: transparent; background: var(--color-surface-sunken); border: 1px solid var(--color-border-strong); border-radius: var(--radius-sm); transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast); } .voice-consent-check > input:checked + .voice-consent-check__box { color: var(--color-primary-text); background: var(--color-primary); border-color: var(--color-primary); } .voice-consent-check > input:focus-visible + .voice-consent-check__box { box-shadow: 0 0 0 3px var(--color-focus-ring); } .voice-consent-check strong { display: block; color: var(--color-text-primary); font-size: var(--text-sm); } .voice-consent-check small { display: block; margin-top: 3px; color: var(--color-text-secondary); font-size: var(--text-xs); line-height: var(--leading-normal); } .voice-create-actions { display: flex; justify-content: flex-end; gap: var(--spacing-sm); padding: var(--spacing-lg) var(--spacing-xl); background: var(--color-bg-secondary); } .voice-readiness { position: sticky; top: var(--spacing-lg); padding: var(--spacing-lg); background: var(--color-bg-secondary); border: 1px solid var(--color-border-default); border-radius: var(--radius-xl); box-shadow: var(--shadow-subtle), var(--shadow-inset-top); } .voice-readiness__header { display: flex; align-items: flex-start; gap: var(--spacing-sm); padding-bottom: var(--spacing-md); border-bottom: 1px solid var(--color-border-divider); } .voice-readiness__icon { width: 34px; height: 34px; flex: 0 0 34px; display: grid; place-items: center; color: var(--color-primary); background: var(--color-primary-light); border-radius: var(--radius-lg); } .voice-readiness__header h2 { margin: 0; font-size: var(--text-base); } .voice-readiness__header p { margin: 3px 0 0; color: var(--color-text-muted); font-size: var(--text-xs); line-height: var(--leading-normal); } .voice-readiness ul { display: flex; flex-direction: column; gap: var(--spacing-xs); list-style: none; padding: var(--spacing-md) 0; margin: 0; } .voice-readiness__item { display: grid; grid-template-columns: 22px minmax(0, 1fr); align-items: center; gap: var(--spacing-sm); min-height: 30px; color: var(--color-text-muted); font-size: var(--text-sm); } .voice-readiness__item i { width: 20px; height: 20px; display: grid; place-items: center; border-radius: var(--radius-full); background: var(--color-surface-sunken); font-size: 0.6rem; } .voice-readiness__item--ready { color: var(--color-text-primary); } .voice-readiness__item--ready i { color: var(--color-success); background: var(--color-success-light); } .voice-readiness__item--warning i { color: var(--color-warning); background: var(--color-warning-light); } .voice-readiness__privacy { display: flex; align-items: flex-start; gap: var(--spacing-sm); padding-top: var(--spacing-md); border-top: 1px solid var(--color-border-divider); color: var(--color-text-muted); } .voice-readiness__privacy i { margin-top: 2px; color: var(--color-primary); } .voice-readiness__privacy p { margin: 0; font-size: var(--text-xs); line-height: var(--leading-normal); } .voice-readiness__privacy strong { color: var(--color-text-secondary); } @media (max-width: 1120px) { .voice-library-shell { grid-template-columns: 1fr; } .voice-library-master { border-right: 0; border-bottom: 1px solid var(--color-border-divider); } .voice-library-list { max-height: 420px; } .voice-create-grid { grid-template-columns: 1fr; } .voice-readiness { position: static; order: -1; } .voice-readiness ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 640px) { .voice-library-page, .voice-create-page { padding: var(--spacing-sm); } .voice-library-page .page-header__meta, .voice-create-page .page-header__meta { width: 100%; } .voice-library-page .page-header__meta .btn, .voice-create-page .page-header__meta .btn { width: 100%; } .voice-library-summary { align-items: flex-start; flex-direction: column; gap: var(--spacing-xs); } .voice-library-summary__divider { display: none; } .voice-library-toolbar { grid-template-columns: 1fr; } .voice-library-language { width: 100%; } .voice-detail__header { flex-direction: column; } .voice-detail__metadata { grid-template-columns: 1fr; } .voice-detail__model-list, .voice-detail__model-fallback, .voice-detail__capability-note { margin-left: 0; } .voice-detail__model-list li { align-items: stretch; flex-direction: column; } .voice-detail__model-list .btn { width: 100%; } .voice-detail__actions { align-items: stretch; flex-direction: column; } .voice-detail__actions .btn { width: 100%; } .voice-detail__actions .btn-danger { margin-left: 0; } .voice-create-section { padding: var(--spacing-lg); } .voice-create-section__heading { grid-template-columns: 1fr; gap: var(--spacing-xs); } .voice-create-actions { padding: var(--spacing-md) var(--spacing-lg); } .voice-create-actions .btn { flex: 1; } .voice-readiness { order: initial; } .voice-readiness ul { grid-template-columns: 1fr; } } /* Nodes: cluster pulse + attention callout (replaces the stat-card strip) */ .cluster-pulse { font-size: var(--text-sm); color: var(--color-text-muted); margin: 0 0 var(--spacing-lg); } .cluster-pulse__strong { color: var(--color-text-primary); font-weight: 600; } .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--warn { background: var(--color-warning-light); border: 1px solid var(--color-warning-border); color: var(--color-text-primary); } .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); } .node-panel { background: var(--color-bg-secondary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); } .node-panel__main { padding: var(--spacing-md) var(--spacing-lg); cursor: pointer; } .node-panel:hover { border-color: var(--color-border); } .node-panel__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--spacing-md); } .node-panel__id { display: flex; align-items: center; gap: var(--spacing-sm); flex-wrap: wrap; } .node-panel__name { font-weight: 600; } .node-panel__meta { display: flex; gap: var(--spacing-lg); margin-top: var(--spacing-sm); color: var(--color-text-muted); font-size: var(--text-xs); } .node-panel__models { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 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; } .node-filter { margin-bottom: var(--spacing-lg); } .node-detail__metrics { display: flex; gap: var(--spacing-xl); margin: var(--spacing-md) 0 var(--spacing-lg); flex-wrap: wrap; } /* 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); border-left: 2px solid var(--color-border-strong); 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-left: 2px solid var(--color-border-default); background: var(--color-surface-sunken); border-radius: 0 var(--radius-md) var(--radius-md) 0; 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; } } /* Live operation card on /app/activity. Status is carried by the icon and the tag rather than a coloured rail, so a page of cards does not read as stripes. */ .operation-card { background: var(--color-bg-secondary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); overflow: hidden; } .operation-card--error { border-color: var(--color-error-border); background: var(--color-error-light); } .operation-card__main { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); } .operation-card__icon { flex: none; } .operation-card__icon--error { color: var(--color-error); } .operation-card__icon--staging { color: var(--color-info); } .operation-card__icon--removing { color: var(--color-warning); } .operation-card__spinner { width: 14px; height: 14px; flex: none; border-radius: 50%; border: 2px solid var(--color-primary-light); border-top-color: var(--color-primary); animation: operationsStripSpin 0.9s linear infinite; } .operation-card__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; } .operation-card__title { display: flex; align-items: center; gap: var(--spacing-xs); flex-wrap: wrap; } .operation-card__name { font-family: var(--font-mono); font-weight: 500; } .operation-card__tag { font-size: 0.625rem; letter-spacing: 0.07em; text-transform: uppercase; font-weight: 600; padding: 1px var(--spacing-xs); border-radius: var(--radius-sm); border: 1px solid var(--color-border-default); color: var(--color-text-muted); } .operation-card__tag--model { color: var(--color-primary); border-color: var(--color-primary-border); background: var(--color-primary-light); } .operation-card__tag--backend { color: var(--color-info); border-color: var(--color-info-border); background: var(--color-info-light); } .operation-card__tag--cluster { color: var(--color-warning); border-color: var(--color-warning-border); background: var(--color-warning-light); } .operation-card__sub { display: flex; align-items: center; gap: var(--spacing-xs); flex-wrap: wrap; font-size: 0.75rem; color: var(--color-text-muted); } .operation-card__bytes { font-variant-numeric: tabular-nums; } /* The legacy installer message embeds an absolute file path, so it is both long and a single unbreakable token. Left unclamped it wraps to three lines and becomes the largest thing on the card, which is how it looked against a real download. One line, ellipsised, full text in the title. */ .operation-card__message { min-width: 0; flex: 1 1 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .operation-card__verb { color: var(--color-text-secondary); } /* A Go error is arbitrarily long and often multi-line. Clamped so it cannot push the rest of the card off screen; the full text is in the title. */ .operation-card__error { color: var(--color-error); font-family: var(--font-mono); /* A flex item defaults to min-width:auto, so one unbroken token (a URL inside a Go error) would refuse to shrink and push the sub row past the card. The vertical clamp below cannot help with that. */ min-width: 0; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; } .operation-card__track { height: 4px; border-radius: var(--radius-full); background: var(--color-surface-sunken); overflow: hidden; } .operation-card__fill { display: block; height: 100%; border-radius: var(--radius-full); background: var(--color-primary); transition: width var(--duration-slow) var(--ease-spring); } .operation-card__actions { display: flex; align-items: center; gap: var(--spacing-xs); flex: none; } .operation-card__pct { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 600; color: var(--color-primary); } .operation-card__hide { 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; } .operation-card__hide:hover { background: var(--color-bg-hover); color: var(--color-text-primary); } .operation-card__nodes-toggle { display: flex; align-items: center; gap: var(--spacing-xs); width: 100%; min-height: 28px; padding: var(--spacing-xs) var(--spacing-sm); border: 0; border-top: 1px solid var(--color-border-subtle); background: transparent; color: var(--color-text-muted); font-size: 0.6875rem; cursor: pointer; } .operation-card__nodes-toggle:hover { color: var(--color-text-secondary); background: var(--color-bg-hover); } /* The node list carries its own inset here. It was written for the old bar, whose parent supplied one, and the card only pads its main row. */ .operation-card .operation-nodes-list { padding: 0 var(--spacing-sm) var(--spacing-xs); } /* One rule between the card body and the node block, not two: when the disclosure is there, it is already the separator. */ .operation-card__nodes-toggle + .operation-nodes-list { margin-top: 0; border-top: 0; } .operation-card .operation-node-error { max-width: 40ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @media (prefers-reduced-motion: reduce) { .operation-card__spinner { animation: none; } .operation-card__fill { transition: none; } .operation-card .operation-node-bar { transition: none; } } /* ── Activity page ────────────────────────────────────────────────────────── */ .activity-page { display: flex; flex-direction: column; gap: var(--spacing-md); } .activity-filters { display: flex; gap: var(--spacing-xs); flex-wrap: wrap; } .activity-chip { min-height: 28px; padding: 0 var(--spacing-sm); border-radius: var(--radius-sm); border: 1px solid var(--color-border-default); background: transparent; color: var(--color-text-muted); font-size: 0.75rem; cursor: pointer; } .activity-chip[aria-pressed="true"] { background: var(--color-primary-light); border-color: var(--color-primary-border); color: var(--color-primary); font-weight: 500; } .activity-section { display: flex; flex-direction: column; gap: var(--spacing-sm); } .activity-section__title { display: flex; align-items: baseline; gap: var(--spacing-xs); margin: 0; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-secondary); } .activity-section__count { font-family: var(--font-mono); color: var(--color-text-muted); font-weight: 400; } .activity-rows { background: var(--color-bg-secondary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); overflow: hidden; } .activity-row { display: grid; grid-template-columns: 16px 1fr auto auto; gap: var(--spacing-sm); align-items: center; min-height: 38px; padding: var(--spacing-xs) var(--spacing-sm); border-bottom: 1px solid var(--color-border-subtle); font-size: 0.8125rem; } .activity-row:last-child { border-bottom: 0; } .activity-row:hover { background: var(--color-bg-hover); } .activity-row__icon--completed { color: var(--color-success); } .activity-row__icon--failed { color: var(--color-error); } .activity-row__icon--cancelled { color: var(--color-warning); } .activity-row__name { font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .activity-row__name small { font-family: var(--font-sans); color: var(--color-text-muted); margin-left: var(--spacing-xs); } .activity-row__when { color: var(--color-text-muted); font-size: 0.75rem; font-variant-numeric: tabular-nums; } .activity-row__action { color: var(--color-primary); font-size: 0.75rem; text-decoration: none; white-space: nowrap; } .activity-row__action:hover { text-decoration: underline; } .activity-note { font-size: 0.75rem; color: var(--color-text-muted); margin: 0; } .activity-empty { display: flex; flex-direction: column; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xl) var(--spacing-md); text-align: center; } .activity-empty__icon { font-size: 1.5rem; color: var(--color-text-disabled); margin-bottom: var(--spacing-xs); } .activity-empty__title { margin: 0; font-weight: 500; color: var(--color-text-secondary); } .activity-empty__body { margin: 0; max-width: 46ch; font-size: 0.8125rem; color: var(--color-text-muted); } /* A chip matching nothing is a smaller event than an empty instance, and gets less of the page so the chip row above stays the obvious thing to change. */ .activity-empty--filtered { padding: var(--spacing-lg) var(--spacing-md); } .nav-badge { min-width: 18px; height: 18px; padding: 0 var(--spacing-xs); border-radius: var(--radius-sm); background: var(--color-primary); color: var(--color-primary-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); color: var(--color-bg-secondary); } /* 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; } /* =========================================================================== Fine-tuning Page-scoped classes for the shapes the shared system does not already cover: the metric charts, the job rows, the GRPO reward pickers and the key/value editor. Everything else on the page (form groups, stat grid, data table, progress bar, callouts) uses the shared classes above rather than restating them inline. =========================================================================== */ /* --- charts ------------------------------------------------------------- */ .ft-chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--spacing-md); margin-bottom: var(--spacing-md); } .ft-chart__head { display: flex; align-items: center; gap: var(--spacing-xs); margin-bottom: var(--spacing-xs); font-size: var(--text-sm); font-weight: var(--font-weight-semibold); } .ft-chart__key { display: inline-block; width: 12px; height: 3px; border-radius: var(--radius-sm); background: var(--ft-chart-color, var(--color-primary)); } .ft-chart__svg { width: 100%; height: auto; max-height: 220px; background: var(--color-bg-secondary); border-radius: var(--radius-sm); } .ft-chart-empty { display: flex; align-items: center; justify-content: center; gap: var(--spacing-xs); min-height: 120px; background: var(--color-bg-secondary); border-radius: var(--radius-sm); font-size: var(--text-sm); color: var(--color-text-muted); } /* --- job rows ----------------------------------------------------------- */ .ft-jobs { display: flex; flex-direction: column; gap: var(--spacing-sm); } .ft-job { cursor: pointer; } .ft-job__head { display: flex; justify-content: space-between; align-items: center; gap: var(--spacing-md); } .ft-job__title { min-width: 0; } .ft-job__backend { margin-left: var(--spacing-sm); font-size: var(--text-sm); color: var(--color-text-muted); } .ft-job__meta, .ft-job__path, .ft-job__message { font-size: var(--text-xs); color: var(--color-text-muted); margin-top: var(--spacing-xs); } .ft-job__path { display: flex; align-items: center; gap: var(--spacing-xs); } .ft-job__message--failed { color: var(--color-error); } /* --- banners ------------------------------------------------------------ */ .ft-banner { display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-secondary); border-radius: var(--radius-sm); font-size: var(--text-sm); } .ft-banner__icon { color: var(--color-primary); } .ft-banner__spacer { margin-left: auto; } /* --- GRPO reward pickers ------------------------------------------------ */ .ft-rewards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--spacing-sm); margin-bottom: var(--spacing-md); } .ft-reward { padding: var(--spacing-sm) var(--spacing-md); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); } .ft-reward--on { border-color: var(--color-primary); background: var(--color-bg-secondary); } .ft-reward__label { display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer; } .ft-reward__name { font-weight: var(--font-weight-semibold); font-size: var(--text-sm); } .ft-reward__desc { font-size: var(--text-xs); color: var(--color-text-muted); } .ft-reward__params { display: flex; flex-direction: column; gap: var(--spacing-xs); margin-top: var(--spacing-sm); padding-left: var(--spacing-lg); } .ft-reward__param { display: flex; align-items: center; gap: var(--spacing-sm); } .ft-reward__param-label { font-size: var(--text-xs); min-width: 80px; } .ft-reward__param .input { width: 100px; font-size: var(--text-sm); } .ft-reward__code { margin: 0; font-size: var(--text-xs); white-space: pre-wrap; color: var(--color-text-muted); } .ft-reward-draft { display: flex; flex-direction: column; gap: var(--spacing-sm); margin-top: var(--spacing-sm); padding: var(--spacing-md); border: 1px dashed var(--color-border-default); border-radius: var(--radius-sm); } .ft-reward-draft .textarea { font-family: var(--font-mono); } /* --- key/value editor --------------------------------------------------- */ .ft-kv { display: flex; flex-direction: column; gap: var(--spacing-sm); align-items: flex-start; } .ft-kv__row { display: flex; gap: var(--spacing-sm); align-items: center; width: 100%; } .ft-kv__key { flex: 1; } .ft-kv__value { flex: 2; } /* --- advanced disclosure ------------------------------------------------ */ .ft-disclosure { display: flex; align-items: center; gap: var(--spacing-sm); width: 100%; padding: 0 0 var(--spacing-sm); background: none; border: none; border-bottom: 1px solid var(--color-border-subtle); font: inherit; font-size: var(--text-xs); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-secondary); cursor: pointer; } .ft-disclosure__chevron { width: 0.75rem; font-size: var(--text-xs); color: var(--color-primary); } .ft-disclosure__icon { color: var(--color-primary); } .ft-advanced { display: flex; flex-direction: column; gap: var(--spacing-md); margin-top: var(--spacing-md); } /* --- export ------------------------------------------------------------- */ .ft-export-status { margin-top: var(--spacing-sm); font-size: var(--text-sm); color: var(--color-success); } .ft-export-status--error { color: var(--color-error); } .ft-export-status__links { display: inline-flex; align-items: center; gap: var(--spacing-sm); margin-left: var(--spacing-sm); } /* --- misc --------------------------------------------------------------- */ .ft-stack { display: flex; flex-direction: column; gap: var(--spacing-md); } .ft-actions { display: flex; gap: var(--spacing-sm); flex-wrap: wrap; } .ft-hint { font-size: var(--text-sm); color: var(--color-text-muted); margin-bottom: var(--spacing-md); } .ft-checkbox { display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer; align-self: end; font-size: var(--text-sm); } .ft-toggle-row { display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); font-size: var(--text-sm); font-weight: var(--font-weight-semibold); } /* Fine-tuning form grids. Named for the group they lay out rather than for their column counts, so the ratios can change without renaming callers. */ .ft-grid-auto { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: var(--spacing-md); } .ft-grid-auto-sm { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--spacing-md); } .ft-grid-model { display: grid; grid-template-columns: 1fr 1fr 2fr; gap: var(--spacing-md); } .ft-grid-dataset { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: var(--spacing-md); } .ft-grid-liquid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--spacing-sm); } @media (max-width: 720px) { .ft-grid-model, .ft-grid-dataset { grid-template-columns: minmax(0, 1fr); } } /* 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; } /* The new-job form is followed directly by the jobs list, which needs the same separation the section rhythm uses everywhere else. .card carries no margin of its own by design. */ .ft-form { margin-bottom: var(--spacing-lg); } /* =========================================================================== 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. =========================================================================== */ .p2p-panel { background: var(--color-bg-secondary); border: 1px solid var(--color-accent-border); border-radius: var(--radius-lg); overflow: hidden; } .p2p-section { padding: var(--spacing-lg); border-bottom: 1px solid var(--color-border-subtle); display: flex; flex-direction: column; gap: var(--spacing-md); } .p2p-section:last-child { border-bottom: 0; } /* 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); } .p2p-token__value { margin: 0; background: var(--color-bg-primary); color: var(--color-warning); padding: var(--spacing-md); border-radius: var(--radius-md); border: 1px solid var(--color-border-subtle); font-family: var(--font-mono); font-size: var(--text-sm); word-break: break-all; white-space: pre-wrap; cursor: pointer; } /* Tabs */ .p2p-tabs { display: flex; gap: 2px; border-bottom: 2px solid var(--color-border-subtle); margin-bottom: var(--spacing-xl); } .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); } .p2p-tab--on { background: var(--color-bg-secondary); border-bottom-color: var(--tone, var(--color-primary)); color: var(--color-text-primary); } .p2p-tab__text { text-align: left; } .p2p-tab__label { font-size: var(--text-base); font-weight: var(--font-weight-semibold); } .p2p-tab__count { font-size: var(--text-xs); color: var(--color-text-muted); } .p2p-tab--on .p2p-tab__count { color: var(--tone); } .p2p-tab:not(.p2p-tab--on) .icon-chip { background: var(--color-bg-tertiary); color: var(--color-text-muted); } /* Architecture diagrams */ .p2p-diagram { background: var(--color-bg-primary); 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); } .p2p-diagram__row { display: flex; align-items: center; justify-content: center; gap: var(--spacing-md); flex-wrap: wrap; } .p2p-diagram__item { text-align: center; display: flex; flex-direction: column; align-items: center; gap: var(--spacing-xs); } .p2p-diagram__label { font-size: var(--text-xs); font-weight: var(--font-weight-semibold); } .p2p-diagram__sub { font-size: 0.625rem; color: var(--color-text-muted); } .p2p-diagram__arrow { display: flex; flex-direction: column; align-items: center; gap: 2px; color: var(--color-text-muted); } .p2p-diagram__arrow span { font-size: 0.625rem; } .p2p-diagram__group { display: flex; gap: var(--spacing-xs); } .p2p-diagram__shard { width: 56px; height: 36px; border-radius: var(--radius-sm); display: grid; place-items: center; background: var(--tone-soft); color: var(--tone); border: 1px solid var(--tone-line); font-size: var(--text-xs); } .p2p-diagram__shard--wide { width: 64px; } .p2p-diagram__shard-label { font-size: 0.5625rem; color: var(--color-text-muted); margin-top: 2px; } .p2p-diagram__note { font-size: var(--text-sm); color: var(--color-text-secondary); text-align: center; line-height: 1.5; margin: 0; } /* Live counts */ .p2p-count { font-size: var(--text-xl); font-weight: var(--font-weight-bold, 700); } .p2p-count__online { color: var(--color-error); } .p2p-count__online--up { color: var(--color-success); } .p2p-count__total { color: var(--color-text-secondary); font-size: var(--text-base); } /* Node grid + card */ .p2p-nodes { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--spacing-md); } .p2p-node { background: var(--color-bg-primary); border: 1px solid var(--color-error-border); border-radius: var(--radius-md); padding: var(--spacing-md); display: flex; flex-direction: column; gap: var(--spacing-sm); transition: border-color var(--duration-normal) var(--ease-default); } .p2p-node--online { border-color: var(--color-success-border); } .p2p-node__name { font-size: var(--text-sm); font-weight: var(--font-weight-semibold); margin: 0; } .p2p-node__id { margin: 0; font-size: var(--text-xs); font-family: var(--font-mono); color: var(--color-text-secondary); word-break: break-all; } .p2p-node__state { display: flex; align-items: center; gap: 6px; background: var(--color-bg-primary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); padding: 4px 10px; font-size: var(--text-xs); font-weight: var(--font-weight-medium); color: var(--color-error); white-space: nowrap; } .p2p-node--online .p2p-node__state { color: var(--color-success); } .p2p-node__state i { font-size: 0.5rem; } .p2p-node__foot { display: flex; align-items: center; gap: 6px; padding-top: var(--spacing-sm); border-top: 1px solid var(--color-border-subtle); font-size: var(--text-xs); color: var(--color-text-muted); } /* Repeated "nothing connected yet" box */ .p2p-empty { text-align: center; padding: var(--spacing-lg); background: var(--color-bg-primary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); } .p2p-empty i { font-size: 2rem; color: var(--color-text-muted); } .p2p-empty p { margin: var(--spacing-sm) 0 0; font-weight: var(--font-weight-medium); color: var(--color-text-secondary); } .p2p-empty p + p { margin-top: var(--spacing-xs); font-size: var(--text-sm); font-weight: 400; color: var(--color-text-muted); } /* Copyable command */ .p2p-cmd { position: relative; } .p2p-cmd pre { margin: 0; background: var(--color-bg-primary); padding: var(--spacing-md); padding-right: var(--spacing-xl); border-radius: var(--radius-md); border: 1px solid var(--color-border-subtle); font-size: var(--text-sm); font-family: var(--font-mono); color: var(--color-warning); white-space: pre-wrap; word-break: break-all; overflow: auto; } /* .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. */ .p2p-cmd__copy { position: absolute; top: 8px; right: 8px; background: var(--color-surface-raised); color: var(--color-text-secondary); border: 1px solid var(--color-border-default); } .p2p-cmd__copy:hover { color: var(--color-primary); border-color: var(--color-primary-border); } /* Numbered setup steps */ .p2p-step { width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; background: var(--tone-soft); color: var(--tone); font-size: var(--text-sm); font-weight: var(--font-weight-bold, 700); flex-shrink: 0; } .p2p-setup { background: var(--color-bg-primary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); padding: var(--spacing-lg); display: flex; flex-direction: column; gap: var(--spacing-sm); } .p2p-setup--warning { border-color: var(--color-warning-border); } .p2p-setup__title { font-size: var(--text-base); font-weight: var(--font-weight-semibold); margin: 0; } /* Callout distinguishing federation from sharding */ .p2p-note { background: var(--color-accent-light); border: 1px solid var(--color-accent-border); border-radius: var(--radius-md); padding: var(--spacing-sm) var(--spacing-md); font-size: var(--text-sm); color: var(--color-text-secondary); } .p2p-note i { color: var(--color-accent); margin-right: 6px; } /* Disabled state */ .p2p-hero { text-align: center; padding: var(--spacing-xl) 0; } .p2p-hero > i { font-size: 3rem; color: var(--color-primary); } .p2p-hero h1 { font-size: var(--text-2xl); font-weight: var(--font-weight-semibold); margin: var(--spacing-md) 0 var(--spacing-sm); } .p2p-hero > p { color: var(--color-text-secondary); max-width: 600px; margin: 0 auto var(--spacing-xl); } .p2p-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: var(--spacing-md); text-align: center; } .p2p-feature { display: flex; flex-direction: column; align-items: center; gap: var(--spacing-xs); } .p2p-feature h3 { font-size: var(--text-base); font-weight: var(--font-weight-semibold); margin: var(--spacing-sm) 0 0; } .p2p-feature p { font-size: var(--text-sm); color: var(--color-text-secondary); margin: 0; } .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; } /* Chart legend swatch. Takes its colour from a .tone-* ancestor or an explicit --tone, so a legend entry never names a data colour twice. */ .legend-dot { display: inline-block; width: 8px; height: 8px; border-radius: var(--radius-sm); background: var(--tone, var(--color-primary)); margin-right: var(--spacing-xs); vertical-align: middle; } /* 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); } /* =========================================================================== Usage dashboard Stat tiles, the token chart with its legend and tooltip, the per-model bar list and the pricing editor. Each was rebuilt inline; named once here. =========================================================================== */ .usage-tile { padding: var(--spacing-sm) var(--spacing-md); flex: 1 1 0; min-width: 120px; } .usage-tile--muted { opacity: 0.7; } .usage-tile__value { font-size: 1.375rem; font-weight: var(--font-weight-bold, 700); font-family: var(--font-mono); color: var(--color-text-primary); } .usage-tile--muted .usage-tile__value { color: var(--color-text-secondary); } .usage-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--spacing-sm); } .usage-grid--narrow { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: var(--spacing-xs); } /* Panels that hang off the top of the chart, squared where they meet it. */ .usage-panel { padding: var(--spacing-md); border-top: 2px solid var(--color-primary); border-top-left-radius: 0; border-top-right-radius: 0; } .usage-panel--muted { opacity: 0.6; } .usage-bar { width: 100%; height: 6px; border-radius: var(--radius-sm); background: var(--color-bg-primary); overflow: hidden; } .usage-bar__fill { height: 100%; border-radius: var(--radius-sm); background: var(--tone, var(--color-primary)); transition: width 0.3s ease; } .usage-split { flex: 1; background: var(--color-bg-primary); border-radius: var(--radius-sm); overflow: hidden; display: flex; } .usage-split__seg { height: 100%; transition: width 0.3s ease; background: var(--tone, var(--color-primary)); } .usage-split__label { width: 120px; min-width: 120px; font-size: var(--text-xs); font-family: var(--font-mono); color: var(--color-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .usage-split__value { min-width: 60px; text-align: right; font-size: var(--text-xs); font-family: var(--font-mono); color: var(--color-text-muted); font-weight: var(--font-weight-semibold); } .chart-host { position: relative; width: 100%; } .chart-legend { display: flex; gap: var(--spacing-md); font-size: var(--text-xs); color: var(--color-text-muted); } .legend-dot--outline { background: transparent; border: 1.5px dashed var(--tone, var(--color-primary)); opacity: 0.6; } .chart-tooltip { position: absolute; background: var(--color-bg-tertiary); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-xs) var(--spacing-sm); font-size: var(--text-xs); font-family: var(--font-mono); color: var(--color-text-primary); pointer-events: none; z-index: 10; box-shadow: var(--shadow-md); white-space: nowrap; } .chart-tooltip__total { color: var(--color-text-muted); border-top: 1px solid var(--color-border); margin-top: 2px; padding-top: 2px; } .usage-price__label { min-width: 70px; font-size: var(--text-xs); color: var(--color-text-muted); font-family: var(--font-mono); } .usage-price__input { flex: 1; max-width: 200px; } .usage-price__out { font-size: var(--text-xs); font-family: var(--font-mono); color: var(--color-text-muted); min-width: 100px; } .toolbar-divider { width: 1px; height: 20px; background: var(--color-border-subtle); margin: 0 var(--spacing-xs); } .usage-filters { display: flex; align-items: flex-end; gap: var(--spacing-md); flex-wrap: wrap; padding: var(--spacing-md); } .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); } /* =========================================================================== Middleware Four near-identical badge components, the router score bars and the score histogram. Backgrounds stay inline because they are data-driven. =========================================================================== */ .mw-badge { display: inline-block; padding: 2px 8px; font-size: var(--text-xs); font-weight: var(--font-weight-semibold); border-radius: var(--radius-sm); font-family: var(--font-mono); text-transform: uppercase; color: #fff; background: var(--color-bg-tertiary); } .mw-noop { font-size: var(--text-xs); font-weight: var(--font-weight-semibold); color: var(--color-warning); white-space: nowrap; cursor: help; } /* Router candidate score bars */ .mw-score { display: flex; align-items: center; gap: var(--spacing-sm); font-family: var(--font-mono); font-size: var(--text-xs); } .mw-score__label { width: 160px; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .mw-score__label--active { color: var(--color-text); font-weight: var(--font-weight-semibold); } .mw-score__track { flex: 1; position: relative; height: 14px; background: var(--color-border); border-radius: 2px; } .mw-score__fill { height: 100%; border-radius: 2px; background: var(--color-text-muted); opacity: 0.45; } .mw-score__fill--active { background: var(--color-success); opacity: 1; } .mw-score__threshold { position: absolute; top: -3px; width: 2px; height: 20px; background: var(--color-warning); transform: translateX(-1px); pointer-events: none; } .mw-score__value { width: 56px; text-align: right; color: var(--color-text-muted); } /* Score histogram under the cache stats */ .mw-hist { display: flex; align-items: flex-end; gap: 1px; margin-top: var(--spacing-xs); height: 18px; } .mw-hist__bar { width: 6px; background: var(--color-border); opacity: 0.3; } .mw-hist__bar--hit { background: var(--color-success); opacity: 1; } .mw-hist__bar--miss { background: var(--color-warning); opacity: 1; } .mw-alert { padding: var(--spacing-md); border-left: 3px solid var(--color-error); } .mw-list { margin: 0; padding-left: 20px; } .mw-list--mono { font-family: var(--font-mono); } .mw-field-input { width: 100%; padding: 8px 12px; font-family: var(--font-mono); font-size: var(--text-base); background: var(--color-bg-tertiary); border: 1px solid var(--color-border-default); } .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; } .mw-badge--off { color: var(--color-text-muted); } .mw-badge--nowrap { white-space: nowrap; } .mw-field-input { border-radius: var(--radius-sm); color: var(--color-text-primary); } .w-12 { width: 12px; } /* =========================================================================== Traces The request/response inspector: field trees, code blocks, metadata grids and the settings banner. The code block in particular appeared five times. =========================================================================== */ .tr-code { margin: 0; background: var(--color-bg-primary); border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: var(--spacing-sm); font-size: var(--text-xs); font-family: var(--font-mono); white-space: pre-wrap; word-break: break-word; overflow: auto; max-height: 50vh; } .tr-well { background: var(--color-bg-primary); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-sm); } .tr-panel { padding: var(--spacing-md); background: var(--color-bg-secondary); border-bottom: 1px solid var(--color-border); } .tr-error { background: var(--color-error-light); border: 1px solid var(--color-error-border); border-radius: var(--radius-md); padding: var(--spacing-sm); display: flex; align-items: center; gap: var(--spacing-xs); } .tr-metrics { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--spacing-xs); font-size: var(--text-xs); } .tr-metrics--fixed { grid-template-columns: repeat(4, 1fr); } .tr-metric { background: var(--color-bg-secondary); border-radius: var(--radius-sm); padding: var(--spacing-xs); } .tr-metric--outlined { background: var(--color-bg-primary); border: 1px solid var(--color-border-subtle); } .tr-meta-grid { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem var(--spacing-md); align-items: baseline; font-size: var(--text-sm); } .tr-fields { border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; } .tr-field { border-bottom: 1px solid var(--color-border); } .tr-field__head { display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); font-size: var(--text-sm); cursor: default; } .tr-field__head--expandable { cursor: pointer; } .tr-field__chevron { width: 12px; flex-shrink: 0; font-size: 0.6rem; color: var(--color-text-secondary); } .tr-field__key { font-family: var(--font-mono); color: var(--color-primary); flex-shrink: 0; } .tr-field__nested { padding: 0 0 var(--spacing-xs) var(--spacing-md); } .tr-field__body { padding: 0 var(--spacing-sm) var(--spacing-sm); } /* Settings banner at the top, tinted by whether tracing is fully on. */ .tr-settings { border: 1px solid var(--color-warning-border); border-radius: var(--radius-md); overflow: hidden; } .tr-settings--ok { border-color: var(--color-success-border); } .tr-settings__toggle { width: 100%; display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-sm) var(--spacing-md); background: var(--color-warning-light); border: none; cursor: pointer; color: var(--color-text-primary); font: inherit; } .tr-settings--ok .tr-settings__toggle { background: var(--color-success-light); } .tr-settings__body { padding: 0 var(--spacing-md) var(--spacing-md); background: var(--color-bg-secondary); border-top: 1px solid var(--color-border-subtle); } .tr-split { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); } .sortable-th { cursor: pointer; user-select: none; } .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; } /* =========================================================================== Backends Tinted notice banners, the summary counters, the backend table's icon / description / badge cells and the detail table under an expanded row. =========================================================================== */ /* A hairline with a coloured left edge, not a tinted card. Same treatment as the Operate overview's attention rows, so "this wants a decision" looks the same wherever it appears. A filled panel makes every notice shout at the weight of an error, which is how notices stop being read. */ .bk-notice { padding: var(--spacing-sm) var(--spacing-md); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; display: flex; align-items: center; gap: var(--spacing-sm); flex-wrap: wrap; background: transparent; border: 1px solid var(--color-border-default); border-left: 3px solid var(--tone, var(--color-primary)); } .bk-notice--between { justify-content: space-between; } .hstack--sm { display: flex; align-items: center; gap: var(--spacing-sm); } .bk-notice__icon { color: var(--tone, var(--color-primary)); } .bk-notice__text { color: var(--tone); font-weight: var(--font-weight-medium); font-size: var(--text-sm); } .bk-counts { display: flex; gap: var(--spacing-md); font-size: var(--text-sm); } .bk-count { font-family: var(--font-mono); font-size: var(--text-xl); font-weight: 600; font-variant-numeric: tabular-nums; color: var(--tone, var(--color-primary)); } .bk-icon-fallback { width: 28px; height: 28px; border-radius: var(--radius-sm); background: var(--color-bg-tertiary); display: grid; place-items: center; } .bk-icon { width: 28px; height: 28px; border-radius: var(--radius-sm); object-fit: cover; } .bk-desc { font-size: var(--text-sm); color: var(--color-text-secondary); display: inline-block; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .bk-manual-grid { display: grid; grid-template-columns: 2fr 1fr 1fr auto; gap: var(--spacing-sm); align-items: end; } .bk-toggles { display: flex; align-items: center; gap: var(--spacing-md); border-left: 1px solid var(--color-border-subtle); padding-left: var(--spacing-md); } .bk-check { display: flex; align-items: center; gap: var(--spacing-xs); font-size: var(--text-xs); color: var(--color-text-secondary); cursor: pointer; } .bk-detail { padding: var(--spacing-md) var(--spacing-lg); background: var(--color-bg-primary); border-top: 1px solid var(--color-border-subtle); } .bk-detail__table { width: 100%; border-collapse: collapse; } .bk-detail__label { font-weight: var(--font-weight-medium); font-size: var(--text-sm); color: var(--color-text-secondary); white-space: nowrap; vertical-align: top; padding: 6px 12px 6px 0; } .bk-detail__value { font-size: var(--text-sm); padding: 6px 0; } .bk-progress { flex: none; width: 120px; margin-top: var(--spacing-xs); } .bk-split-btn { padding: 0 8px; border-left: 1px solid rgba(0, 0, 0, 0.15); border-top-left-radius: 0; border-bottom-left-radius: 0; } .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); } /* =========================================================================== Talk (realtime voice) Status banner, the pipeline-model summary, the transcript pane and the diagnostics panel. Status colour stays inline because it is state-driven. =========================================================================== */ .talk-page { display: flex; flex-direction: column; align-items: center; } .talk-col { width: 100%; max-width: 48rem; } .talk-status { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--radius-md); } .talk-hint { background: var(--color-primary-light); border: 1px solid color-mix(in srgb, var(--color-primary) 20%, transparent); border-radius: var(--radius-md); padding: var(--spacing-sm) var(--spacing-md); display: flex; align-items: flex-start; gap: var(--spacing-sm); } .talk-check { display: flex; align-items: center; gap: var(--spacing-xs); font-size: var(--text-sm); cursor: pointer; color: var(--color-text); } .talk-check--locked { cursor: not-allowed; color: var(--color-text-secondary); } .talk-chip { background: var(--color-bg-secondary); border-radius: var(--radius-sm); padding: var(--spacing-xs) var(--spacing-sm); border: 1px solid var(--color-border); font-size: var(--text-xs); display: flex; align-items: center; gap: var(--spacing-xs); } .talk-slots { display: flex; flex-direction: column; gap: var(--spacing-xs); font-size: var(--text-xs); } .talk-slot { background: var(--color-bg-secondary); border-radius: var(--radius-sm); padding: var(--spacing-xs); border: 1px solid var(--color-border); display: flex; align-items: baseline; gap: var(--spacing-sm); } .talk-slot__value { font-family: var(--font-mono); min-width: 0; margin-left: auto; text-align: right; overflow-wrap: anywhere; } .talk-details { border: 1px solid var(--color-border); border-radius: var(--radius-md); } .talk-details > summary { cursor: pointer; padding: var(--spacing-sm) var(--spacing-md); font-weight: var(--font-weight-medium); color: var(--color-text-secondary); font-size: var(--text-base); } .talk-details__body { padding: var(--spacing-md); padding-top: var(--spacing-xs); display: flex; flex-direction: column; gap: var(--spacing-md); } .talk-transcript { max-height: 24rem; overflow-y: auto; min-height: 6rem; padding: var(--spacing-sm); background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-md); display: flex; flex-direction: column; gap: var(--spacing-xs); } .talk-line { display: flex; align-items: flex-start; gap: var(--spacing-xs); } .talk-line__icon { margin-top: 3px; flex-shrink: 0; font-size: var(--text-xs); } .talk-line__text { margin: 0; } .talk-line__text--tool { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--color-text-secondary); } .talk-line__text--result { white-space: pre-wrap; } .talk-diag { border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-md); } .talk-diag__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--spacing-xs); font-size: var(--text-xs); } .talk-diag__cell { background: var(--color-bg-secondary); border-radius: var(--radius-sm); padding: var(--spacing-xs); } .talk-diag__raw { margin: 0; font-size: var(--text-xs); color: var(--color-text-secondary); background: var(--color-bg-secondary); border-radius: var(--radius-sm); padding: var(--spacing-xs); max-height: 8rem; overflow-y: auto; font-family: var(--font-mono); white-space: pre-wrap; } .talk-canvas { width: 100%; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface-sunken); } .talk-split { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-sm); } .btn--accent { background: var(--color-accent); color: var(--color-primary-text); border: none; } .btn--error { background: var(--color-error); color: var(--color-text-inverse); border: none; } .ml-auto { margin-left: auto; } /* =========================================================================== Import model The page is one form, not two modes. Its shape is a work column plus a format reference: the reference answers the only question a first-time admin has ("what do I paste?"), and it used to sit behind a disclosure that was closed by default. Below --bp-tablet the column cannot hold, so the component swaps it for a disclosure rather than dropping it. No wells and no nested cards here. Fields group by proximity and a label, the way the rest of the restyled app does. =========================================================================== */ .form-hint-sm { margin-top: var(--spacing-xs); font-size: var(--text-xs); color: var(--color-text-muted); } .import-split { display: grid; grid-template-columns: minmax(0, 1fr) 19rem; gap: var(--spacing-2xl); align-items: start; } .import-split--stacked { grid-template-columns: minmax(0, 1fr); } .import-main { min-width: 0; } .import-aside { min-width: 0; } /* --- the source field ---------------------------------------------------- */ /* The one control on the page that 90% of visits touch, so it is the largest one and it carries its own commit button. The focus ring lives on the bar rather than the input because the button shares the frame. */ .import-source__bar { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--spacing-sm); align-items: stretch; background: var(--color-surface-sunken); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); padding: 4px 4px 4px 0; transition: border-color var(--duration-normal) var(--ease-spring), box-shadow var(--duration-normal) var(--ease-spring); } .import-source__bar:focus-within { border-color: var(--color-border-primary); box-shadow: 0 0 0 3px var(--color-primary-light); } .import-source__input { min-width: 0; background: transparent; border: 0; outline: none; color: var(--color-text-primary); /* A source is something you paste or type verbatim, not prose. Same rule the lanes use for model ids and backend names. */ font-family: var(--font-mono); font-size: var(--text-lg); padding: var(--spacing-sm) var(--spacing-md); } .import-source__input::placeholder { color: var(--color-text-disabled); } .import-source__input:disabled { color: var(--color-text-disabled); } .import-source__btn { min-height: 38px; padding-inline: var(--spacing-lg); } /* --- disclosures --------------------------------------------------------- */ .import-disclosure { background: none; border: 0; padding: 0; margin-top: var(--spacing-md); color: var(--color-text-secondary); cursor: pointer; font: inherit; font-size: var(--text-sm); display: flex; align-items: center; gap: var(--spacing-sm); } .import-disclosure:hover { color: var(--color-text-primary); } .import-options__summary { color: var(--color-text-tertiary); font-size: var(--text-xs); font-family: var(--font-mono); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .import-options { margin-top: var(--spacing-lg); border-top: 1px solid var(--color-border-default); padding-top: var(--spacing-xs); } /* Two columns of fields at this width, which is the whole reason the page moved off the narrow archetype: nine preferences in one column is 1,900px of scroll. Wide fields (backend, description, custom prefs) span both. */ .import-options__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--spacing-lg) var(--spacing-xl); margin-top: var(--spacing-lg); } .import-field { min-width: 0; } .import-field--wide { grid-column: 1 / -1; } /* A hint that introduces a control rather than trailing it. */ .import-field__lead { margin-top: 0; margin-bottom: var(--spacing-sm); } .import-check { display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer; font-size: var(--text-base); color: var(--color-text-secondary); } .import-check__hint { margin-left: 26px; } .import-custom-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: var(--spacing-sm); margin-top: var(--spacing-sm); } /* --- modality chips ------------------------------------------------------ */ .modality-chips { display: flex; gap: var(--spacing-xs); flex-wrap: wrap; margin-bottom: var(--spacing-sm); } .modality-chip { flex-shrink: 0; padding: 3px var(--spacing-sm); border-radius: var(--radius-sm); border: 1px solid var(--color-border-default); background: transparent; color: var(--color-text-muted); font-family: inherit; font-size: var(--text-xs); cursor: pointer; transition: background var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default), border-color var(--duration-fast) var(--ease-default); } .modality-chip:hover:not(:disabled) { color: var(--color-text-primary); border-color: var(--color-border-strong); } .modality-chip.is-active { background: var(--color-primary-light); border-color: var(--color-primary-border); color: var(--color-primary); font-weight: var(--font-weight-medium); } .modality-chip:disabled { cursor: not-allowed; opacity: 0.5; } /* --- estimate ------------------------------------------------------------ */ /* A result of the field above, not a page banner. The rule on the leading edge attaches it to what produced it. */ .import-estimate { display: flex; gap: var(--spacing-xl); flex-wrap: wrap; margin-top: var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-tertiary); border: 1px solid var(--color-border-subtle); border-left: 2px solid var(--color-primary); border-radius: 0 var(--radius-md) var(--radius-md) 0; } .import-estimate__cell { display: flex; flex-direction: column; gap: 1px; } .import-estimate__k { font-size: var(--text-xs); font-weight: var(--font-weight-bold); letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-text-tertiary); } .import-estimate__v { font-family: var(--font-mono); font-size: var(--text-lg); color: var(--color-text-primary); font-variant-numeric: tabular-nums; } /* --- in-flight import ---------------------------------------------------- */ /* The poller already reports progress, phase and byte counts; the page used to render only `message`. Same visual grammar as the Activity cards. */ .import-progress { margin-top: var(--spacing-md); padding: var(--spacing-md); background: var(--color-bg-tertiary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); display: flex; flex-direction: column; gap: var(--spacing-sm); } .import-progress__row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--spacing-md); } .import-progress__name { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--color-text-primary); min-width: 0; overflow: hidden; text-overflow: ellipsis; } .import-progress__pct { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--color-success); font-variant-numeric: tabular-nums; } .import-progress__track { height: 3px; background: var(--color-surface-sunken); border-radius: var(--radius-full); overflow: hidden; } .import-progress__fill { display: block; height: 100%; background: var(--color-success); border-radius: var(--radius-full); transition: width var(--duration-slow) var(--ease-default); } .import-progress__meta { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--color-text-tertiary); } /* --- format reference ---------------------------------------------------- */ .import-aside .import-formats { border-left: 1px solid var(--color-border-default); padding-left: var(--spacing-lg); } .import-formats__head { margin: 0 0 var(--spacing-sm); font-size: var(--text-xs); font-weight: var(--font-weight-bold); letter-spacing: 0.07em; text-transform: uppercase; color: var(--color-text-tertiary); } .import-formats__list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--color-border-subtle); } .import-formats__row { display: flex; flex-direction: column; gap: 2px; padding: var(--spacing-sm) var(--spacing-xs); border-bottom: 1px solid var(--color-border-subtle); } .import-formats__kind { font-size: var(--text-xs); font-weight: var(--font-weight-bold); letter-spacing: 0.05em; text-transform: uppercase; color: var(--color-primary); } .import-formats__uri { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--color-text-secondary); word-break: break-all; } /* The scheme is the part you have to get right; the rest is a placeholder. */ .import-formats__uri b { font-weight: var(--font-weight-medium); color: var(--color-text-primary); } .import-formats__uri em { font-style: normal; color: var(--color-text-tertiary); } .import-formats__foot { margin: var(--spacing-md) 0 0; font-size: var(--text-sm); } /* --- YAML surface -------------------------------------------------------- */ .import-yaml__head { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-md); flex-wrap: wrap; padding-bottom: var(--spacing-sm); border-bottom: 1px solid var(--color-border-default); margin-bottom: var(--spacing-md); } @media (max-width: 640px) { .import-options__grid { grid-template-columns: minmax(0, 1fr); } .import-source__bar { grid-template-columns: minmax(0, 1fr); } .import-custom-row { grid-template-columns: minmax(0, 1fr); } } /* =========================================================================== 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; } .pill-tiny { padding: 2px 6px; font-size: var(--text-xs); } /* =========================================================================== Settings The sticky section rail, the scrolling content column, the branding asset preview and the resource meters. Meter colour is computed from usage. =========================================================================== */ .set-layout { display: flex; gap: 0; min-height: calc(100vh - 180px); } .set-rail { width: 180px; 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%; padding: 8px 12px; background: transparent; border: none; border-left: 2px solid transparent; border-radius: var(--radius-md); cursor: pointer; color: var(--color-text-secondary); font: inherit; font-size: var(--text-sm); 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); border-left-color: var(--color-primary); } .set-rail__icon { width: 16px; text-align: center; font-size: var(--text-xs); color: var(--color-text-muted); } .set-content { flex: 1; overflow: auto; padding: 0 var(--spacing-lg) var(--spacing-xl) var(--spacing-md); max-height: calc(100vh - 180px); } .set-asset { width: 56px; height: 56px; display: grid; place-items: center; background: var(--color-surface-elevated); border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; } .set-asset img { max-width: 100%; max-height: 100%; object-fit: contain; } .set-meters { background: var(--color-bg-tertiary); border-radius: var(--radius-md); padding: var(--spacing-sm); font-size: var(--text-xs); } .set-meter { display: flex; align-items: center; gap: var(--spacing-xs); } .set-meter__label { color: var(--color-text-muted); min-width: 60px; } .set-meter__track { flex: 1; height: 6px; background: var(--color-bg-primary); border-radius: var(--radius-sm); overflow: hidden; } .set-meter__fill { height: 100%; border-radius: var(--radius-sm); } .set-meter__value { min-width: 40px; text-align: right; } .set-pct { font-size: var(--text-base); font-weight: var(--font-weight-semibold); min-width: 40px; text-align: right; } .set-head { padding: var(--spacing-lg) var(--spacing-lg) 0; } .w-280 { width: 280px; } /* =========================================================================== 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); } .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); } .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; } /* =========================================================================== Agent job details The collapsible trace steps (tinted by trace kind, so colours stay inline) and the code blocks for prompt / result / error payloads. =========================================================================== */ .ajd-trace { border-radius: var(--radius-md); overflow: hidden; } .ajd-trace__toggle { width: 100%; display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-sm) var(--spacing-md); background: none; border: none; cursor: pointer; color: var(--color-text-primary); text-align: left; font: inherit; } .ajd-trace__index { font-size: var(--text-xs); font-weight: var(--font-weight-bold, 700); color: var(--color-text-muted); background: var(--color-bg-secondary); border-radius: var(--radius-sm); padding: 2px 6px; min-width: 24px; text-align: center; } .ajd-trace__body { padding: 0 var(--spacing-md) var(--spacing-sm); font-size: var(--text-sm); } .ajd-pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--color-text-secondary); line-height: 1.6; } .ajd-pre--nested { margin: var(--spacing-xs) 0 0; line-height: 1.5; } .ajd-code { background: var(--color-bg-primary); padding: var(--spacing-sm); border-radius: var(--radius-md); font-size: var(--text-sm); white-space: pre-wrap; overflow: auto; max-height: 200px; } .ajd-polling { text-align: center; padding: var(--spacing-md); color: var(--color-text-muted); font-size: var(--text-sm); } .ajd-webhook { display: flex; align-items: center; gap: var(--spacing-sm); background: var(--color-bg-primary); border-radius: var(--radius-md); padding: var(--spacing-sm); } .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-md); } @media (max-width: 720px) { .grid-3 { grid-template-columns: minmax(0, 1fr); } } .badge--md { font-size: var(--text-base); padding: 4px 12px; } .ajd-code--tall { max-height: 300px; } .ajd-code--taller { max-height: 500px; } .ajd-code--error { background: var(--color-error-light); color: var(--color-error); max-height: none; } /* ========================================================================== SplitView: the shell shared by Discover, Backends and Host. 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-default); border-radius: var(--radius-lg); overflow: hidden; } .entity-rail__head { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm); border-bottom: 1px solid var(--color-border-subtle); flex-wrap: wrap; } .entity-rail__count { font-size: 0.75rem; 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: 0.6875rem; color: var(--color-text-muted); background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm); padding: 2px 6px; 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: 6px var(--spacing-sm); background: var(--color-surface-sunken); border: 0; border-top: 1px solid var(--color-border-subtle); 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.5625rem; color: var(--color-text-muted); width: 10px; } .entity-rail__group-icon { font-size: 0.6875rem; color: var(--color-text-muted); } .entity-rail__group-label { font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; } .entity-rail__group-count { margin-left: auto; font-family: var(--font-mono); font-size: 0.6875rem; color: var(--color-text-muted); font-variant-numeric: tabular-nums; } .entity-rail__item { display: flex; align-items: center; gap: var(--spacing-sm); width: 100%; padding: 7px var(--spacing-sm); background: transparent; border: 0; border-top: 1px solid var(--color-border-divider); 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; } } .entity-rail__item--on { background: var(--color-primary-light); box-shadow: inset 2px 0 0 var(--color-primary); } .entity-rail__icon { flex: none; width: 16px; font-size: 0.75rem; color: var(--color-text-muted); text-align: center; } .entity-rail__item--on .entity-rail__icon { color: var(--color-primary); } .entity-rail__main { min-width: 0; display: flex; flex-direction: column; gap: 1px; } .entity-rail__name { font-size: 0.8125rem; font-weight: 500; color: var(--color-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .entity-rail__meta { font-family: var(--font-mono); font-size: 0.6875rem; color: var(--color-text-tertiary); font-variant-numeric: tabular-nums; } /* A left edge 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: 3px; align-self: stretch; border-radius: 2px; 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: 0.8125rem; 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-default); border-radius: var(--radius-lg); min-height: 420px; padding: var(--spacing-md); } .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: 0.625rem; letter-spacing: 0.11em; text-transform: uppercase; color: var(--color-eyebrow); } .zero-pane__title { font-size: 1.0625rem; font-weight: 600; letter-spacing: -0.015em; text-wrap: balance; } .zero-pane__text { font-size: 0.8125rem; 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: 0.875rem; font-weight: 600; } .zero-pane__shelf-meta { margin-left: auto; font-family: var(--font-mono); font-size: 0.6875rem; color: var(--color-text-muted); } .zero-pane__shelf-hint { font-size: 0.8125rem; 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-bg-primary); border: 1px solid var(--color-border-default); 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: linear-gradient(150deg, var(--color-primary-light), transparent 68%), var(--color-bg-primary); } .zero-pane__tile-name { font-size: 0.8125rem; font-weight: 550; } .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: 0.8125rem; padding: 6px var(--spacing-sm); border-radius: var(--radius-sm); 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: 0.625rem; 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; font-size: 0.75rem; color: var(--color-text-muted); background: transparent; border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); padding: 3px 8px; 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(--color-primary); padding-top: 2px; } .detail-pane__title { flex: 1 1 260px; min-width: 0; } .detail-pane__name { font-size: 1.0625rem; font-weight: 600; letter-spacing: -0.015em; word-break: break-word; } .detail-pane__lede { font-size: 0.8125rem; color: var(--color-text-muted); margin-top: 2px; } .detail-pane__actions { display: flex; gap: var(--spacing-xs); align-items: center; flex: none; } .discover__progress { flex: none; width: 120px; margin-top: 4px; } .detail-pane__warning { display: flex; align-items: center; gap: 6px; font-size: 0.75rem; color: var(--color-error); background: var(--color-error-light); border: 1px solid var(--color-error-border); border-radius: var(--radius-sm); padding: 6px var(--spacing-sm); } .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1px; background: var(--color-border-default); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); overflow: hidden; } .stat-grid__item { background: var(--color-surface-sunken); padding: 6px var(--spacing-sm); } .stat-grid__item dt { font-family: var(--font-mono); font-size: 0.5625rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-text-muted); } .stat-grid__item dd { font-size: 0.9375rem; font-weight: 600; 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-size: 0.625rem; 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. Used by the Host status page for "loaded now" and by detail panes for anything that is a list of facts rather than a table. */ .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; } /* The filter band moved into the rail column, so it stacks instead of laying out three horizontal bands across the page. It sits there because it narrows the rail and nothing else: one column to say what you want, one to show what you got. */ .split-view__rail-col .models-filters { margin-bottom: 0; } .split-view__rail-col .models-filters__query, .split-view__rail-col .models-filters__refine { flex-direction: column; align-items: stretch; gap: var(--spacing-xs); } .split-view__rail-col .models-filters__backend { width: 100%; } /* Nineteen chips do not fit beside a 360px rail. The trigger states the selection and the popover carries the full set, which also stops the chip row and the rail competing to be the same control. */ .models-filters__usecase-trigger { display: flex; align-items: center; gap: var(--spacing-xs); width: 100%; padding: 6px var(--spacing-sm); background: var(--color-bg-secondary); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); color: var(--color-text-primary); font-size: 0.8125rem; cursor: pointer; text-align: left; } .models-filters__usecase-trigger:hover { border-color: var(--color-border-strong); } .models-filters__usecase-caret { margin-left: auto; font-size: 0.625rem; color: var(--color-text-muted); } .models-filters__usecases { margin: 0; padding: var(--spacing-xs) 0 0; max-height: 46vh; overflow-y: auto; } /* ========================================================================== 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 shell above is overflow:hidden so the document cannot grow, which left anything taller than the viewport simply unreachable — Host has a resources card, four stat cards and a tab bar above its split, and at any window height the bottom of the pane fell off with nothing to scroll. 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: 1.375rem; font-weight: 600; letter-spacing: -0.02em; } .view-bar__count { font-family: var(--font-mono); font-size: 0.75rem; color: var(--color-text-muted); font-variant-numeric: tabular-nums; } .view-bar__actions { display: flex; gap: var(--spacing-xs); 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; } /* Grouped chips. Four families and a reset, rather than nineteen in a row. */ .models-filters__usecase-group { display: flex; flex-direction: column; gap: 4px; } .models-filters__usecase-group + .models-filters__usecase-group { margin-top: var(--spacing-sm); padding-top: var(--spacing-sm); border-top: 1px solid var(--color-border-divider); } .models-filters__usecase-label { font-family: var(--font-mono); font-size: 0.625rem; letter-spacing: 0.11em; text-transform: uppercase; color: var(--color-text-muted); } .models-filters__usecases .filter-bar { margin: 0; } /* The refinements read as a section with a name, not three controls left where they landed when the band became a column. */ .split-view__rail-col .models-filters__refine { margin-top: var(--spacing-sm); padding-top: var(--spacing-sm); border-top: 1px solid var(--color-border-divider); gap: 6px; } .models-filters__refine-label { font-family: var(--font-mono); font-size: 0.625rem; letter-spacing: 0.11em; text-transform: uppercase; color: var(--color-text-muted); } .split-view__rail-col .filter-bar-group__toggle { justify-content: flex-start; width: 100%; padding: 2px 0; } .split-view__rail-col .models-filters__context { display: flex; align-items: center; gap: var(--spacing-xs); width: 100%; } .split-view__rail-col .models-filters__context input[type='range'] { flex: 1; min-width: 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; } } /* Backends: the filters stack in the rail column the way Discover's do. Seven chips fit at this width, so they need no disclosure. */ .bk-filters { display: flex; flex-direction: column; gap: var(--spacing-xs); margin-bottom: var(--spacing-sm); } .bk-filters .search-bar { width: 100%; } .bk-filters .filter-bar { margin: 0; } .split-view__rail-col .bk-toggles { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; } /* Host keeps its resource monitor, summary cards and tabs above the split, so only what is left after them is pinned. Those are the page's own chrome and they are read once, unlike the rail and the pane, which are worked in. */ .page--app > .tabs { flex: none; } .page--app > .view-bar { flex: none; } /* The console layout is a flex row with align-items:flex-start, so its body sizes to content. That is right for the pages it was built for and wrong for a split view, which needs a ceiling to scroll inside: without it the rail ran past the viewport and over the footer. Scoped with :has() so only the split-view routes are pinned; every other console page keeps sizing to its content. */ .console-layout:has(.page--app) { flex: 1; min-height: 0; align-items: stretch; } .console-layout:has(.page--app) > .console-body { min-height: 0; display: flex; flex-direction: column; } @media (max-width: 900px) { .console-layout:has(.page--app) { flex: none; align-items: flex-start; } .console-layout:has(.page--app) > .console-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 Discover, Host, 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-- .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-sm) 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(--color-primary); font-size: 0.6875rem; 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: 0.875rem; font-weight: 500; } .lane__desc { color: var(--color-text-muted); font-size: 0.8125rem; } /* Identifiers and quantities are monospace; prose is not. */ .lane__num { font-family: var(--font-mono); font-size: 0.75rem; color: var(--color-text-tertiary); font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; } .lane__go { color: var(--color-text-tertiary); } /* 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
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: 0.9375rem; font-weight: 700; 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 console shell, 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%; } /* Home: resident models and the header figures. */ .lanes--resident .lane { grid-template-columns: minmax(0, 1fr) minmax(0, 9rem) auto auto; } .lanes--jump .lane { grid-template-columns: minmax(0, 8rem) minmax(0, 1fr) auto; } .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); } .home-jump { margin-bottom: var(--spacing-2xl); } .home-loaded-stop { border: 0; background: transparent; color: var(--color-text-tertiary); cursor: pointer; padding: 2px 6px; border-radius: var(--radius-sm); } .home-loaded-stop:hover { color: var(--color-error); background: var(--color-error-light); } .home-stat { display: flex; align-items: baseline; gap: var(--spacing-xs); } .home-stat__value { font-family: var(--font-mono); font-size: 1.125rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--color-text-primary); line-height: 1; } .home-stat__value--ok { color: var(--color-success); } .home-stat__label { font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--color-text-muted); } /* Headline figures on the Operate overview, and the sparklines under them. Hairline-gridded cells rather than cards: the same 1px grid the split-view StatGrid uses, so the two read as one system. */ /* A row of headline figures on a hairline grid. Shared by the Operate overview and the Host page so the two read as one system: same cell, same figure scale, same tone vocabulary. Replaces Host's shadowed clickable cards, which were a second dashboard language on a page that already had one. */ .stat-strip, .operate-headline { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1px; margin: 0 0 var(--spacing-xl); background: var(--color-border-default); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); overflow: hidden; } .stat-strip__cell, .operate-headline__cell { background: var(--color-bg-primary); padding: var(--spacing-sm) var(--spacing-md); display: grid; gap: 2px; } .stat-strip__label, .stat-strip__cell dt, .operate-headline__cell dt { color: var(--color-text-muted); font-size: 0.625rem; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; } .stat-strip__value, .operate-headline__value { margin: 0; font-family: var(--font-mono); font-size: 1.5rem; font-weight: 600; line-height: 1.1; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } .stat-strip__value--primary, .operate-headline__value--primary { color: var(--color-text-primary); } .stat-strip__value--success, .operate-headline__value--success { color: var(--color-success); } .stat-strip__value--warning, .operate-headline__value--warning { color: var(--color-warning); } .stat-strip__value--muted, .operate-headline__value--muted { color: var(--color-text-tertiary); } button.stat-strip__cell { border: 0; color: inherit; text-align: left; font: inherit; cursor: pointer; transition: background var(--duration-fast) var(--ease-default); } button.stat-strip__cell:hover { background: var(--color-bg-hover); } button.stat-strip__cell:focus-visible { outline: 2px solid var(--color-focus-ring); outline-offset: -2px; } .sparkline { display: block; width: 100%; height: 28px; margin-top: 2px; } .sparkline polyline { stroke: currentColor; } .sparkline circle { fill: currentColor; } .sparkline--primary { color: var(--color-primary); } .sparkline--success { color: var(--color-success); } .sparkline--warning { color: var(--color-warning); } .sparkline--muted { color: var(--color-text-disabled); } /* A signal changing as a poll lands should read as an update, not a jump cut. Opacity only, so it stays on the compositor and cannot reflow the rail. */ .nav-signal { transition: opacity var(--duration-normal) var(--ease-default); } /* The attention block is the one thing on the page that should announce itself. A short, single reveal on the row's left edge, not on the text. */ @keyframes attentionEdge { from { border-left-color: transparent; } to { border-left-color: var(--color-warning); } } .lanes--attention .lane { animation: attentionEdge var(--duration-reveal) var(--ease-reveal) both; } @media (prefers-reduced-motion: reduce) { .nav-signal { transition: none; } .lanes--attention .lane { animation: none; } } /* 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); } .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; } /* Latency in a trace row: a bar plus the figure. The bar is scaled against the slowest request in view so the tail is visible while scanning, and it turns amber past the slow threshold — the same constraint tone used elsewhere. */ .lat { display: grid; grid-template-columns: minmax(2.5rem, 1fr) auto; align-items: center; gap: var(--spacing-xs); } .lat__bar { height: 4px; border-radius: 2px; background: var(--color-bg-tertiary); overflow: hidden; } .lat__bar i { display: block; height: 100%; background: var(--color-primary); } .lat__bar--slow i { background: var(--color-warning); } .lat b { font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 500; color: var(--color-text-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; } /* 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, 6rem) minmax(0, 1fr) minmax(0, 7rem) minmax(0, 6rem) auto; } .lane__tag--evidence { color: var(--color-warning); } @media (max-width: 720px) { .lanes--recommended .lane { grid-template-columns: minmax(0, 1fr) auto; } .lanes--recommended .lane__num { display: none; } } /* ─── Operate console: rail signals and the overview ────────────────────── The signal is ambient state beside a rail label, not a badge: no pill, no fill, just a dimmer tabular figure a scanning eye can ignore. The alarm-shaped treatment belongs to .nav-badge on the sidebar, which stays visible when this rail is collapsed or absent. */ .nav-signal { margin-left: auto; flex: none; font-family: var(--font-mono); font-size: 0.6875rem; color: var(--color-text-tertiary); font-variant-numeric: tabular-nums; } .nav-item.active .nav-signal { color: var(--color-primary); } /* One line, deliberately quiet. Making "nothing is wrong" as loud as a problem is how a status page teaches people to stop reading it. */ .operate-clear { margin: 0; color: var(--color-text-muted); font-size: 0.875rem; } .lanes--attention .lane { grid-template-columns: minmax(0, auto) minmax(0, 1fr) auto; } .lanes--sections .lane { grid-template-columns: minmax(0, 10rem) minmax(0, 1fr) auto; } /* Amber on the row itself rather than a status chip: these are the only things on the page asking for a decision, so they can afford the one accent. */ .lanes--attention .lane { border-left: 3px solid var(--color-warning); } /* ─── Studio: capability dots ───────────────────────────────────────────── Filled means a model on this machine serves that modality. aria-hidden, because the overview states the same thing in words and a dot that changes as models load would otherwise interrupt a reader mid-strip. */ .studio-tab__dot { width: 6px; height: 6px; border-radius: 50%; flex: none; margin-left: var(--spacing-xs); } .studio-tab__dot--on { background: var(--color-success); } .studio-tab__dot--off { background: transparent; box-shadow: inset 0 0 0 1px var(--color-border-strong); } /* Room for the description to sit on one line at a comfortable width instead of wrapping into a narrow column beside empty space. */ .lanes--modality .lane { grid-template-columns: 6rem minmax(0, 1fr) minmax(0, 14rem) 5rem auto; } .lanes--takes .lane { grid-template-columns: minmax(0, 1fr) auto; } .studio-modality--empty { cursor: default; } .studio-modality__install { color: var(--color-primary); font-size: 0.8125rem; font-weight: 700; white-space: nowrap; } .studio-modality__state { color: var(--color-success); font-size: 0.8125rem; font-weight: 600; } .studio-running__meter { height: 4px; border-radius: 2px; background: var(--color-bg-tertiary); overflow: hidden; width: 8rem; } .studio-running__meter i { display: block; height: 100%; background: var(--color-primary); } .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; }