/* Settings and administration screens. */ /* --- Page scaffolding ------------------------------------------------------ One scroll container per screen, and this is the rule that decides which. `.admin-scroll` is it on every admin page. `.tabs__body` is it only where the tabs are a *bounded* flex child -- `.main > .tabs` on the settings page -- which is why the selector below says so rather than naming the class alone. It used to name the class alone, and the result was two scrollers stacked on /admin/prompts, where `.tabs` sits inside `.admin-scroll > .admin-page`, a plain block. `flex: 1` and `min-height: 0` mean nothing there, so `.tabs__body` had `height: auto` and never scrolled while still declaring `overflow-y: auto` -- and everything written to reset "the scroller" reset that one, silently, while the reader was lost in the other. Under `.admin-scroll` the body is now an ordinary block and the page scrolls as one. */ .admin-scroll, .main > .tabs > .tabs__body { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; } .page, .admin-page { max-width: 48rem; margin: 0 auto; padding: var(--sp-6) var(--sp-5) var(--sp-12); } /* A directory chosen rather than typed. The path is the only part allowed to shrink, and it truncates -- an absolute path on somebody else's machine is long enough to push the Clear button off the card otherwise. */ .dir-row { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; } .dir-row > .btn { min-width: 0; } .dir-row__path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); font-size: var(--text-xs); } .page-header { margin-bottom: var(--sp-6); } .admin-lede, .page-header__lede { color: var(--ink-muted); font-size: var(--text-sm); line-height: var(--leading-relaxed); margin: 0 0 var(--sp-6); max-width: 44rem; } .admin-section-title, .section-title { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-lg); margin: var(--sp-8) 0 var(--sp-4); } /* --- Tabs ------------------------------------------------------------------ Radio inputs plus sibling selectors: no JavaScript, and the browser keeps the chosen tab across a re-render. */ .tabs { display: flex; flex-direction: column; flex: 1; min-height: 0; } .tabs__bar { display: flex; align-items: center; gap: var(--sp-1); padding: 0 var(--sp-5); border-bottom: 1px solid var(--border); background: var(--bg); flex: none; overflow-x: auto; scrollbar-width: none; /* Sticky where the *page* is the scroller, which is the admin layout. The Tools panel is longer than a screen, so without this the bar scrolls away and changing tab means scrolling back up to find it. Harmless on the settings page, where `.tabs__body` scrolls underneath a bar that never moves anyway. It needs the opaque `background` above it already has, or the panel would show through. `z-index` because a panel's own cards establish stacking contexts and would otherwise paint over it. */ position: sticky; top: 0; z-index: 1; } .tabs__tab { display: inline-flex; align-items: center; gap: var(--sp-2); height: var(--control-h-lg); padding: 0 var(--sp-4); border-bottom: 2px solid transparent; color: var(--ink-muted); font-size: var(--text-sm); font-weight: 500; cursor: pointer; white-space: nowrap; transition: color var(--transition-fast), border-color var(--transition-fast); } .tabs__tab:hover { color: var(--ink); } /* The library's tabs are links between pages rather than radios, so the active one is marked server-side. Same bar, same look. */ .tabs__tab.is-active { color: var(--ink); border-bottom-color: var(--accent); } a.tabs__tab { text-decoration: none; } .tabs__panel { display: none; } /* The active tab's label. Each radio is immediately followed by its own label, so this needs to know nothing about how many tabs there are or what they are called. */ .tabs__bar input:checked + .tabs__tab { color: var(--ink); border-bottom-color: var(--accent); } /* The active panel, matched by position. CSS cannot compare a radio's id with a panel's data-tab, so this used to name every tab twice -- and a tab added without also adding its two rules here rendered a label that selected nothing. That is not a failure anyone spots in review; it looks like a blank page. Position is derivable, so it is used instead: the Nth radio shows the Nth panel. Both lists are rendered in the same order, and a conditional tab drops out of both at once, so they cannot drift apart. :nth-of-type counts by element name, which is why the panels are
and the alerts above them are
-- the alerts are not counted. Enumerated to eight, comfortably more than exist. A ninth tab needs one line. */ .tabs__bar:has(input:nth-of-type(1):checked) ~ .tabs__body .tabs__panel:nth-of-type(1), .tabs__bar:has(input:nth-of-type(2):checked) ~ .tabs__body .tabs__panel:nth-of-type(2), .tabs__bar:has(input:nth-of-type(3):checked) ~ .tabs__body .tabs__panel:nth-of-type(3), .tabs__bar:has(input:nth-of-type(4):checked) ~ .tabs__body .tabs__panel:nth-of-type(4), .tabs__bar:has(input:nth-of-type(5):checked) ~ .tabs__body .tabs__panel:nth-of-type(5), .tabs__bar:has(input:nth-of-type(6):checked) ~ .tabs__body .tabs__panel:nth-of-type(6), .tabs__bar:has(input:nth-of-type(7):checked) ~ .tabs__body .tabs__panel:nth-of-type(7), .tabs__bar:has(input:nth-of-type(8):checked) ~ .tabs__body .tabs__panel:nth-of-type(8) { display: block; } .tabs__tab:has(:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; } /* A form's action row, and the space after the form it closes. Not a plain .btn-row: a settings form is a stack of cards, and Save has to read as belonging to the cards above it rather than to whatever comes next. The gap that matters is the one *after* the form -- a card following it (the "Try it" panel on the search page, say) otherwise sits flush against Save and looks like another field of the same form. That margin therefore belongs to the form, not to the action row: the action row is always its form's last child, so a bottom margin there would have nothing to push away from. */ .form-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-5); } .admin-page > form { margin-bottom: var(--sp-8); } .admin-page > form:last-child { margin-bottom: 0; } /* --- Cards ----------------------------------------------------------------- */ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--sp-5); margin-bottom: var(--sp-4); } .card:last-child { margin-bottom: 0; } .card__title { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-md); margin-bottom: var(--sp-2); } .card__lede { color: var(--ink-muted); font-size: var(--text-sm); margin-bottom: var(--sp-4); line-height: var(--leading-relaxed); } .card__footer { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--border); flex-wrap: wrap; } .card__header { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-4); flex-wrap: wrap; } /* Two or three short fields on one line. Numbers with their own units — width and height, steps and cfg and denoise — read as a set and take a fraction of the width each, so stacking them makes a card that is mostly whitespace and a form that is mostly scrolling. Auto-fit rather than a fixed count: the same markup gives three columns on a wide screen and one on a narrow one with no breakpoint, which matters because `chat.css` has a test refusing media queries and this file should not drift from that habit without a reason. */ .field-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: var(--sp-3); } .field-row > .field { margin-bottom: var(--sp-4); } /* Legacy aliases so existing admin templates keep their spacing. */ .form-grid { display: block; } .connection__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-4); flex-wrap: wrap; } .connection__footer { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--border); flex-wrap: wrap; } .field--actions { margin-top: var(--sp-5); margin-bottom: 0; } /* --- Definition lists ------------------------------------------------------ */ .detail-list { display: grid; grid-template-columns: minmax(6rem, auto) 1fr; gap: var(--sp-2) var(--sp-4); margin: 0; font-size: var(--text-sm); } .detail-list dt { color: var(--ink-muted); font-weight: 500; } .detail-list dd { margin: 0; } /* --- Status ---------------------------------------------------------------- */ .status-dot { width: 0.55rem; height: 0.55rem; border-radius: var(--radius-full); flex: none; background: var(--ink-faint); } .status-dot.is-ok { background: var(--success); } .status-dot.is-bad { background: var(--danger); } .status-dot.is-off { background: var(--ink-faint); } /* --- Filter bar ------------------------------------------------------------ */ .filter-bar { display: flex; flex-direction: column; gap: var(--sp-3); margin-bottom: var(--sp-4); } .filter-tabs { display: flex; gap: var(--sp-1); flex-wrap: wrap; border-bottom: 1px solid var(--border); } .filter-tab { display: inline-flex; align-items: center; gap: var(--sp-2); height: var(--control-h); padding: 0 var(--sp-3); border-bottom: 2px solid transparent; color: var(--ink-muted); font-size: var(--text-sm); font-weight: 500; text-decoration: none; white-space: nowrap; } .filter-tab:hover { color: var(--ink); } .filter-tab.is-active { color: var(--ink); border-bottom-color: var(--accent); } .filter-tab__count { font-size: var(--text-xs); color: var(--ink-faint); background: var(--surface-active); border-radius: var(--radius-full); padding: 0 0.4rem; min-width: 1.4rem; text-align: center; } .filter-tab.is-active .filter-tab__count { background: var(--accent-soft); color: var(--accent); } .filter-form { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; } .filter-form .input { width: auto; flex: 1; min-width: 12rem; } .filter-form .select { width: auto; min-width: 10rem; } /* --- Model list ------------------------------------------------------------ Compact rows only. Editing is a page of its own -- a connection can advertise a hundred models, and a list that renders a form for each is unusable. */ .bulk-bar { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-lg) var(--radius-lg) 0 0; border-bottom: 0; background: var(--bg-sunken); } .bulk-bar__label { font-size: var(--text-sm); color: var(--ink-muted); } .model-rows { border: 1px solid var(--border); border-radius: 0 0 var(--radius-lg) var(--radius-lg); overflow: hidden; background: var(--surface); } .model-row { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); } .model-row:last-child { border-bottom: 0; } .model-row:hover { background: var(--surface-hover); } .model-row.is-off { opacity: 0.55; } .model-row__check { accent-color: var(--accent); width: 1rem; height: 1rem; flex: none; } .model-row__pos { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-faint); min-width: 1.75rem; text-align: right; flex: none; } .model-row__avatar { flex: none; width: 1.75rem; height: 1.75rem; } .model-row__main { flex: 1; min-width: 0; } .model-row__title { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; } .model-row__name { color: var(--ink); font-weight: 500; font-size: var(--text-sm); text-decoration: none; } .model-row__name:hover { color: var(--accent); text-decoration: underline; } .model-row__id { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; } .model-row__actions { display: flex; align-items: center; gap: var(--sp-1); flex: none; } .list-footer { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-3); flex-wrap: wrap; } /* --- Model detail ---------------------------------------------------------- */ .crumbs { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-4); font-size: var(--text-sm); } .crumbs > a:first-child { display: inline-flex; align-items: center; gap: var(--sp-1); color: var(--ink-muted); text-decoration: none; } .crumbs > a:first-child:hover { color: var(--ink); } .crumbs__back { transform: rotate(180deg); } .model-detail__avatar { width: 3rem; height: 3rem; flex: none; } .model-list { list-style: none; margin: 0; padding: 0; } .model-list__item { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-3) 0; border-bottom: 1px solid var(--border); } .model-list__item:first-child { padding-top: 0; } .model-list__item:last-child { border-bottom: 0; padding-bottom: 0; } /* --- Permission grids ------------------------------------------------------ */ .checkbox-row { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); } .checkbox-row .checkbox { flex: 0 0 auto; } .checkbox.is-muted { opacity: 0.6; } .perm-row { align-items: flex-start; padding: var(--sp-3) 0; border-bottom: 1px solid var(--border); } .perm-row:last-child { border-bottom: 0; } .perm-row input { margin-top: 0.15rem; } .perm-row__desc { display: block; font-size: var(--text-xs); color: var(--ink-faint); font-weight: 400; line-height: var(--leading-normal); margin-top: 0.1rem; } .perm-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); } .perm-list__item { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-sm); } .perm-list__state { display: grid; place-items: center; width: 1.25rem; height: 1.25rem; border-radius: var(--radius-full); background: var(--surface-active); color: var(--ink-faint); flex: none; } .perm-list__state.is-on { background: var(--success-soft); color: var(--success); } /* --- Reference lists ------------------------------------------------------- The variable legend on the prompts page, and the read-only tool list beneath it. A grid rather than a table because both have to collapse to a stack on a narrow screen, which a table cannot do without abandoning its header. */ .ref-list { display: flex; flex-direction: column; } .ref-row { display: grid; grid-template-columns: minmax(7rem, 11rem) 1fr minmax(0, 12rem); gap: var(--sp-3); align-items: baseline; padding: var(--sp-2) 0; border-top: 1px solid var(--border); font-size: var(--text-sm); line-height: var(--leading-normal); } .ref-row:first-child { border-top: 0; padding-top: 0; } .ref-row > code { overflow-wrap: anywhere; } .ref-row__value { color: var(--ink-faint); font-size: var(--text-xs); overflow-wrap: anywhere; } @media (max-width: 44rem) { .ref-row { grid-template-columns: 1fr; gap: var(--sp-1); } } /* The assembled prompt. Wraps rather than scrolls sideways -- it is prose, and a horizontal scrollbar on prose is unreadable. */ .prompt-preview { margin: 0 0 var(--sp-3); padding: var(--sp-3); max-height: 28rem; overflow-y: auto; white-space: pre-wrap; overflow-wrap: anywhere; background: var(--code-bg); border: 1px solid var(--code-border); border-radius: var(--radius); font-family: var(--font-mono); font-size: var(--text-xs); line-height: var(--leading-normal); color: var(--ink-muted); } .prompt-preview code { font-family: inherit; background: none; border: 0; padding: 0; } /* --- Misc ------------------------------------------------------------------ */ .input--file { height: auto; padding: 0.35rem; font-size: var(--text-xs); background: var(--surface-raised); } .nav-item.is-disabled { opacity: 0.4; cursor: default; } .nav-item.is-disabled:hover { background: none; color: var(--ink-muted); } .field__hint code, .card__lede code { font-family: var(--font-mono); font-size: 0.92em; padding: 0.05em 0.3em; border-radius: var(--radius-sm); background: var(--code-bg); border: 1px solid var(--code-border); } /* --- The permission modes, explained on the agents page ------------------- */ .mode-list { margin: 0; display: flex; flex-direction: column; gap: var(--sp-2); } .mode-list__row { display: flex; gap: var(--sp-3); align-items: baseline; } .mode-list__row dt { flex: 0 0 5rem; color: var(--ink); } .mode-list__row dd { margin: 0; color: var(--ink-muted); font-size: var(--text-sm); } /* --- The schedule setup form ------------------------------------------------ */ /* Which "when" fieldset is showing follows the radio, with no JavaScript. `:has()` is what makes that possible, and it is the same move the "Something else" box on an approval card makes: a control and the thing it reveals cannot fall out of step if the stylesheet is the only thing relating them. Hidden with `display: none` on the container rather than `[hidden]`, which would need `!important` here -- see the note in app.css about `.btn` being `inline-flex`. Fields inside a hidden fieldset are still submitted, which is correct: `_rule_from_form` reads only the keys the chosen repeat mode uses. */ .schedule-repeat { border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-4); margin-bottom: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-3); } .schedule-repeat > legend { padding: 0 var(--sp-2); } .schedule-repeat [data-repeat] { display: none; } .schedule-repeat:has(input[value="every"]:checked) [data-repeat="every"] { display: block; } .schedule-repeat:has(input[value="calendar"]:checked) [data-repeat="calendar"] { display: block; } /* A row of buttons above a list. `.btn-row` carries no bottom margin -- it is used inside forms where `.field` provides the rhythm -- so "Add a workflow" sat flush against the first row of the list it adds to, and the two read as one control. Stated as an adjacency rather than a margin on the button row, because the row is right to have none everywhere else it appears. */ .btn-row + .model-rows, .btn-row + .model-list { margin-top: var(--sp-4); }