/* ------------------------------------------------------------------ *
 *  Systematic Utility — admin UI stylesheet (base rules + components).
 *
 *  Design tokens (colors/typography/radii/spacing) are NOT here: they live
 *  in static/ui/themes/<theme>.css, generated from themes/manifest_*.py by
 *  `codegen.py -f css` and linked directly from templates/base.html. Theme is
 *  driven by the `data-theme` attribute on <html>.
 * ------------------------------------------------------------------ */

/* ----------------------------- base ----------------------------- */
* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  background: var(--background);
  color: var(--on-background);
  font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

code, .mono, .id { font-family: "JetBrains Mono", ui-monospace, monospace; }

a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }

h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 .25rem; }
h2 { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; margin: 0 0 .25rem; }
h3 { font-size: 16px; font-weight: 600; margin: 0; }

.label {
  text-transform: uppercase;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--on-surface-variant);
}
.muted { color: var(--on-surface-variant); }

/* --------------------------- app shell -------------------------- */
.app { display: flex; min-height: 100vh; }

.sidebar {
  width: 240px;
  flex-shrink: 0;
  background: var(--surface-container-low);
  border-right: 1px solid var(--outline-variant);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;            /* brand fixed, nav scrolls, account pinned */
  transition: width .15s ease;
}

/* ---- Collapsed sidebar: an icon-only rail. Toggled by the topbar button
   (toggleSidebar in base.html) and remembered in localStorage. ---- */
body.sidebar-collapsed .sidebar { width: 64px; }
body.sidebar-collapsed .sidebar .brand { justify-content: center; padding-left: 0; padding-right: 0; }
body.sidebar-collapsed .sidebar .brand > span,
body.sidebar-collapsed .sidebar .brand .help-icon,
body.sidebar-collapsed .nav-section,
body.sidebar-collapsed .nav-link > span,
body.sidebar-collapsed .nav-group .twirl,
body.sidebar-collapsed .nav-group-row > .twirl,
body.sidebar-collapsed .nav-group-row .help-icon,
body.sidebar-collapsed .nav-children,
body.sidebar-collapsed .account-name { display: none !important; }
body.sidebar-collapsed .nav-link,
body.sidebar-collapsed .nav-group-row { justify-content: center; }
body.sidebar-collapsed .nav-link { padding-left: .5rem; padding-right: .5rem; }
body.sidebar-collapsed .account-user { padding-left: 0; padding-right: 0; }
body.sidebar-collapsed .account-user h2 { justify-content: center; }
body.sidebar-collapsed .sidebar-account { align-items: center; }
.sidebar .brand {
  display: flex; align-items: center; gap: .6rem;
  padding: 1rem 1.25rem;
  font-weight: 600;
  border-bottom: 1px solid var(--outline-variant);
  flex-shrink: 0;
}

/* Manifest help link — a muted question-mark icon shown next to an element
   (project brand, and later schema/table/column headers). Brightens on hover;
   pushed to the trailing edge when it sits in the brand row. */
.help-icon {
  display: inline-flex; align-items: center; justify-content: center;
  vertical-align: middle;      /* aligns the icon when it trails an <h1> text run */
  margin-left: .3rem;          /* small gap after the label it annotates */
  color: var(--on-surface-variant); opacity: .6;
  text-decoration: none; flex-shrink: 0;
}
.help-icon:hover { opacity: 1; color: var(--primary); }
.help-icon i { width: 16px; height: 16px; }
/* lucide icon inherited onto a form section header (column-group ui_icon) — sized
   down to sit inline with the small uppercase heading text. */
.section-icon { width: 14px; height: 14px; vertical-align: middle; }
/* lucide icon (column ``ui_icon``) rendered before a column's label in grid
   headers / form field labels / filter labels. Inherits the label colour. */
.col-icon { width: 14px; height: 14px; vertical-align: middle; margin-right: .3rem; }
/* table ``ui_icon`` before the grid's <h1> title — sized to the 24px heading. */
.grid-title-icon { width: 1.25rem; height: 1.25rem; vertical-align: middle; margin-right: .4rem; }
.sidebar .brand .help-icon { margin-left: auto; }
/* the menu is the only scrolling region; brand + account stay put */
.sidebar nav { padding: .5rem; display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; overflow-y: auto; min-height: 0; }

/* Account block pinned to the sidebar bottom */
.sidebar-account {
  flex-shrink: 0;
  border-top: 1px solid var(--outline-variant);
  padding: .5rem;
  display: flex; flex-direction: column; gap: 2px;
}
.account-user {
  padding: .6rem .75rem; min-width: 0;
}
/* the markup wraps avatar + name in an <h2>; reset its heading defaults and make
   it a left-aligned row so the avatar comes first with the username inline beside
   it, both on the same level (vertically centred) */
.account-user h2 {
  margin: 0; font: inherit;
  display: flex; align-items: center; gap: .6rem;
  min-width: 0;
}
.account-avatar {
  width: 32px; height: 32px; border-radius: 50%;
  object-fit: cover; flex-shrink: 0;
  background: var(--surface-container);
}
.account-avatar-fallback {
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--on-surface-variant);
}
.account-avatar-fallback svg { width: 18px; height: 18px; }
.account-name {
  font-weight: 600; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nav-link {
  display: flex; align-items: center; gap: .6rem;
  padding: .55rem .75rem;
  border-radius: .375rem;
  color: var(--on-surface);
  font-weight: 500;
}
.nav-link:hover { background: var(--surface-container); text-decoration: none; }
.nav-link.active { background: var(--primary); color: var(--on-primary); }
.nav-section { padding: .75rem .75rem .25rem; }

/* expandable nav groups */
[x-cloak] { display: none !important; }
/* Any nav entry that is a <button> rather than a link — the group headers, and
   the permission tools, which open a modal instead of navigating. A button is
   used so the keyboard can reach them at all (an <a> with no href is neither
   focusable nor Enter-activatable); this strips the button chrome back to a
   nav-link's look. */
button.nav-link {
  width: 100%; border: none; background: transparent; cursor: pointer;
  font: inherit; font-weight: 500; text-align: left; color: var(--on-surface);
}
/* The menu is walked with Alt+K and the arrows, so the focused entry has to be
   visible — the row cursor gets the same treatment on table rows. */
.nav-link:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.nav-group .twirl { margin-left: auto; display: inline-flex; transition: transform .15s ease; }
.nav-group .twirl.open { transform: rotate(90deg); }
/* Group header row: the toggle button grows to fill, the help ? and the expand
   twirl trail it (twirl lives outside the button so the help link — an <a> —
   isn't nested inside a <button>, which is invalid). */
.nav-group-row { display: flex; align-items: center; }
.nav-group-row .nav-group { flex: 1 1 auto; width: auto; }
.nav-group-row > .twirl {
  display: inline-flex; align-items: center; cursor: pointer;
  transition: transform .15s ease; color: var(--on-surface-variant);
}
.nav-group-row > .twirl.open { transform: rotate(90deg); }
.nav-children { display: flex; flex-direction: column; gap: 2px; padding-left: .85rem; }
.nav-children .nav-link { font-weight: 400; }

.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem;
  padding: .75rem 1.5rem;
  border-bottom: 1px solid var(--outline-variant);
  background: var(--surface-container-lowest);
  position: sticky; top: 0; z-index: 20;
}
.content { padding: 1.5rem; flex: 1; }
/* CMS pages render their own full-bleed sections (backgrounds span the whole
   width; inner content is centered/padded by base.css's .cms-section__inner), so
   drop the admin content padding when a CMS page is shown inside the shell. */
.app:has(.cms-page) .content { padding: 0; }

/* ---------------------------- cards ----------------------------- */
.card {
  background: var(--surface-container-lowest);
  border: 1px solid var(--outline-variant);
  border-radius: .5rem;
  box-shadow: var(--shadow);
}
.card-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--outline-variant);
}
.card-body { padding: 1.25rem; }
/* ACE code editor (JSON / code fields). ACE auto-sizes via min/maxLines; the
   container just supplies the frame and rounded corners. */
.ace-editor {
  width: 100%;
  min-height: 9rem;
  border: 1px solid var(--outline-variant);
  border-radius: .5rem;
  font-size: 13px;
  line-height: 1.4;
}
.card-footer {
  display: flex; align-items: center; justify-content: flex-end;
  gap: .5rem;
  padding: 1rem 1.25rem;
  border-top: 1px solid var(--outline-variant);
}

/* --------------------------- buttons ---------------------------- */
.btn {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .5rem .9rem;
  border-radius: .25rem;
  border: 1px solid transparent;
  font: inherit; font-weight: 600; font-size: 13px;
  cursor: pointer;
  background: var(--primary); color: var(--on-primary);
  transition: filter .12s ease, background .12s ease;
}
.btn:hover { filter: brightness(1.08); text-decoration: none; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-ghost { background: transparent; color: var(--on-surface); border-color: var(--outline-variant); }
.btn-ghost:hover { background: var(--surface-container); filter: none; }
.btn-danger { background: var(--error); color: var(--on-error); }
.btn-sm { padding: .3rem .6rem; font-size: 12px; }
.btn-icon { padding: .4rem; background: transparent; color: var(--on-surface-variant); border-color: transparent; }
.btn-icon:hover { background: var(--surface-container); color: var(--on-surface); }
/* an action that doesn't apply to this row (see _action_off.html): still drawn,
   so same-type icons stay aligned across rows and the user can see the action
   exists, but greyed out and inert. */
.btn-icon:disabled { opacity: .35; cursor: not-allowed; color: var(--on-surface-variant); }
.btn-icon:disabled:hover { background: transparent; color: var(--on-surface-variant); }

/* ---------------------------- forms ----------------------------- */
.field { display: flex; flex-direction: column; gap: .3rem; margin-bottom: 1rem; }
.field > label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--on-surface-variant); }
.input, .select, .textarea {
  width: 100%;
  padding: .5rem .65rem;
  background: var(--surface-container-lowest);
  border: 1px solid var(--outline-variant);
  border-radius: .25rem;
  color: var(--on-surface);
  font: inherit; font-size: 14px;
}
/* Dropdown options: pair background and text explicitly so the popup never
   ends up same-on-same (option styling is honored where the browser allows it;
   color-scheme above covers the rest). */
.select option { background: var(--surface-container-lowest); color: var(--on-surface); }
.input::placeholder, .textarea::placeholder { color: var(--on-surface-variant); opacity: .7; }
.textarea { min-height: 80px; resize: vertical; font-family: "JetBrains Mono", monospace; font-size: 13px; }
.input:focus, .select:focus, .textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 25%, transparent); }
/* an input holding a rejected value (e.g. a calculated column whose expression
   doesn't compile — the message renders under the field) */
.input-error, .input.input-error { border-color: var(--error); }
.input-error:focus { border-color: var(--error); box-shadow: 0 0 0 2px color-mix(in srgb, var(--error) 25%, transparent); }

/* Clearable search box (templates/_search_form.html, _manifest_grid.html): an
   in-field "x" button that appears only while there's a query (the .has-text
   class is toggled by the input, or rendered server-side) and wipes the search. */
.search-field { position: relative; display: inline-block; }
.search-field .input { padding-right: 1.85rem; }
/* suppress the browser's native search clear so we don't render two x's */
.search-field .input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.search-field .search-clear {
  position: absolute; top: 50%; right: .4rem; transform: translateY(-50%);
  display: block; align-items: center; justify-content: center;
  padding: 2px; border: 0; background: transparent; cursor: pointer;
  color: var(--on-surface-variant); line-height: 0;
}
.search-field.has-text .search-clear { display: inline-flex; }
.search-field .search-clear:hover { color: var(--on-surface); }
.search-field .search-clear svg, .search-field .search-clear i { width: 15px; height: 15px; }
.checkbox { width: 16px; height: 16px; accent-color: var(--primary); }
.form-row { display: flex; gap: 1rem; }
.form-row > * { flex: 1; }

/* on/off switch (permission-overview matrix toggles) */
.switch { position: relative; display: inline-block; width: 38px; height: 22px; flex: 0 0 auto;
  border: none; border-radius: 999px; background: var(--outline-variant); cursor: pointer; padding: 0;
  transition: background .15s ease; vertical-align: middle; }
.switch .switch-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.3); transition: left .15s ease; }
.switch.on { background: var(--primary); }
.switch.on .switch-knob { left: 18px; }
.switch:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* labelled switch row (e.g. the admin Settings modal) */
.switch-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: .6rem 0; border-bottom: 1px solid var(--outline-variant); font-size: 14px; }
.switch-row:last-child { border-bottom: none; }
.switch-row svg { width: 16px; height: 16px; color: var(--on-surface-variant); }

/* permission-overview matrix: sticky header row + sticky first column */
.perm-matrix th, .perm-matrix td { white-space: nowrap; }
.perm-matrix thead th { position: sticky; top: 0; z-index: 2; background: var(--surface-container-low, var(--surface-container-lowest)); }
.perm-matrix .sticky-col { position: sticky; left: 0; z-index: 1; background: var(--surface-container-lowest); text-align: left; }
.perm-matrix thead th.sticky-col { z-index: 3; }

/* Tabulator (editable grid modal) — blend the light-only base theme with the
   app's surface/on-surface variables so it works in dark mode too. */
.tabulator { background: var(--surface-container-lowest); border-color: var(--outline-variant); color: var(--on-surface); font-family: inherit; font-size: 13px; }
.tabulator .tabulator-header, .tabulator .tabulator-header .tabulator-col { background: var(--surface-container-low, var(--surface-container-lowest)); color: var(--on-surface); border-color: var(--outline-variant); }
.tabulator .tabulator-row { background: var(--surface-container-lowest); color: var(--on-surface); }
.tabulator .tabulator-row.tabulator-row-even { background: var(--surface-container, var(--surface-container-lowest)); }
.tabulator .tabulator-row .tabulator-cell { border-color: var(--outline-variant); }
/* taller rows (~50% more than Tabulator's default) — more vertical padding so
   the row grows with content rather than being clipped to a fixed height. */
.tabulator .tabulator-row .tabulator-cell { padding-top: 11px; padding-bottom: 11px; }
.tabulator .tabulator-row:hover { background: var(--surface-container-high, var(--surface-container)); }
.tabulator .tabulator-footer { background: var(--surface-container-low, var(--surface-container-lowest)); color: var(--on-surface); border-color: var(--outline-variant); }
.tabulator .tabulator-cell.tabulator-editing input, .tabulator .tabulator-cell.tabulator-editing select { background: var(--surface-container-lowest); color: var(--on-surface); border-color: var(--primary); }
.tabulator-edit-list { background: var(--surface-container-lowest); color: var(--on-surface); border: 1px solid var(--outline-variant); }
.tabulator-edit-list .tabulator-edit-list-item { color: var(--on-surface); }
.tabulator-edit-list .tabulator-edit-list-item.active, .tabulator-edit-list .tabulator-edit-list-item:hover { background: var(--primary); color: var(--on-primary); }

/* --------------------------- tables ----------------------------- */
.table-wrap { overflow-x: auto; }
table.data { width: 100%; border-collapse: collapse; font-size: 13px; --bulk-col-w: 2rem; }
table.data thead th {
  position: sticky; top: 0; z-index: 2;
  text-align: left;
  text-transform: uppercase;
  font-size: 11px; font-weight: 600; letter-spacing: .03em;
  color: var(--on-surface-variant);
  padding: .55rem .75rem;
  background: var(--surface-container);
  border-bottom: 1px solid var(--outline-variant);
  white-space: nowrap;
}
/* A header whose manifest column carries a description shows it as a native
   tooltip on hover (_column_head.html) — the help cursor is the affordance. */
table.data thead th.has-desc { cursor: help; }
table.data tbody td { padding: .5rem .75rem; border-bottom: 1px solid var(--outline-variant); vertical-align: top; }
/* Every row carries an opaque background of its own, so the frozen columns
   below can inherit it (see "frozen columns"). It is the surface the row was
   already sitting on — a card, a modal — so nothing looks different. */
table.data tbody tr { background: var(--surface-container-lowest); }
table.data tbody tr:hover { background: var(--surface-container-low); }
table.data .id { font-size: 12px; color: var(--on-surface-variant); }
/* bookmark toggle on list rows: outline when off, filled amber when on */
.bookmark-toggle.on { color: var(--warning); }
.bookmark-toggle.on svg { fill: currentColor; }
/* Lucide ships strokes only, no filled variants — so an on/off toggle reads as
   outline vs filled by painting the stroke icon's interior (same trick as the
   bookmark above). Shape, not just colour, so the state survives a glance, a
   colour-blind reader and a greyscale print. Used by the query builder's
   default-view stars (templates/_search_library_list.html). */
.icon-toggle.on { color: var(--primary); }
.icon-toggle.on svg { fill: currentColor; }
/* boolean cells render as icons: check (true) / minus (false) */
table.data .bool-icon { width: 24px; height: 24px; vertical-align: middle; }
table.data tbody tr.clickable { cursor: pointer; }

/* ------------------------ bulk selection ------------------------- */
/* The checkbox column (templates/_bulk_bar.html) and the bar that acts on it.
   The column is as narrow as its box and never grows; the cell drops the default
   top alignment so the box sits on the text baseline of the row.
   Its width is stated rather than left to the content because the drag handle
   beside it pins at exactly this offset (see "frozen columns"): --bulk-col-w is
   what the column measured anyway — .75rem of padding + a 16px box + .25rem. */
table.data th.bulk-col, table.data td.bulk-col {
  width: var(--bulk-col-w); min-width: var(--bulk-col-w);
  white-space: nowrap; padding-right: .25rem; vertical-align: middle;
}
table.data td.bulk-col { cursor: default; }
/* The Actions cell wraps after six buttons instead of running off to the right.
   A row can carry a lot of them — file analytics, bookmark, preview, editor,
   tree, view, run, every custom action, plus edit/delete — and on one line the
   frozen column ate a third of a wide window and pushed the data columns out.

   Six is the cap because it is about as many icons as read as a group at a glance,
   and two rows of six is still shorter than most rows are tall, so wrapping costs
   no row height in practice.

   The nowrap above is what fixed the column's minimum at "every button on one
   line"; the cell releases it and the flex container below takes over that job,
   with a maximum instead of a minimum. The header keeps nowrap so "Actions" itself
   never wraps.

   The buttons are pinned to their natural square (a 24px lucide icon + .4rem
   padding + a 1px border) rather than left to size themselves, so the width below
   is arithmetic rather than a guess: get it wrong by a pixel and the sixth button
   wraps or a seventh joins the line. */
table.data td.actions-col { white-space: normal; }
/* Grid rather than flex-wrap, because the column is `width: 1%` — it sizes to its
   content's intrinsic width. A wrapping flex container's intrinsic minimum is its
   widest single item, so the cell collapsed to one button per line; six explicit
   tracks give it an intrinsic width of exactly six buttons, which is the number we
   want. Tracks are `auto`, so a row with fewer than six buttons leaves its unused
   tracks at zero width instead of reserving space for buttons that aren't there. */
.row-actions {
  --row-action-size: 2.425rem;  /* 24px + 2×.4rem + 2×1px */
  --row-action-gap: 1px;
  display: grid;
  grid-template-columns: repeat(6, auto);
  justify-content: end;
  gap: var(--row-action-gap);
  margin-left: auto;
}
.row-actions > .btn-icon {
  width: var(--row-action-size);
  height: var(--row-action-size);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
/* A ticked row is tinted, so the selection is readable at a glance while
   scrolling past the checkbox column. Mixed into the surface rather than into
   `transparent`: the frozen columns inherit this colour and would let the
   columns sliding underneath show through a translucent one. */
table.data tbody tr.bulk-selected { background: color-mix(in srgb, var(--primary) 10%, var(--surface-container-lowest)); }
table.data tbody tr.bulk-selected:hover { background: color-mix(in srgb, var(--primary) 16%, var(--surface-container-lowest)); }

/* ------------------------ frozen columns ------------------------- */
/* What a row IS scrolls sideways; what you DO to it does not — the row controls
   pin to the edges: checkbox and drag handle left, actions right, so a wide
   table can be read across without losing the tick box, the grip or the row
   buttons. All three are of fixed width, unlike the data columns between them,
   so pinning them costs no readable width.
   Two things make it work: a sticky cell must be opaque (it inherits the row
   background set above, hover / selection / focus tint included), and the
   sticky header must outrank the sticky body cells — hence the same z-index
   ladder .perm-matrix uses: body 1 < header 2 < the header's own frozen cells 3. */
/* The other two control columns hold buttons, not data, so they are worth exactly
   what they contain — at any window width. `width: 1%` is how a column shrink-wraps
   inside a width:100% auto-layout table: it is under the content's minimum, so the
   browser falls back to that minimum and hands every spare pixel to the data columns
   instead of sharing it out proportionally (which is what made the actions column
   grow a field of empty space on a wide window and snap tight on a narrow one).
   `white-space: nowrap` is what fixes that minimum at "all the buttons on one line" —
   the cells already state it inline; the headers need it too, or a widened window
   would let "Actions" wrap instead. */
table.data th.actions-col, table.data td.actions-col,
table.data th.row-drag-col, table.data td.row-drag-col { width: 1%; white-space: nowrap; }
table.data th.bulk-col, table.data td.bulk-col,
table.data th.row-drag-col, table.data td.row-drag-col,
table.data th.actions-col, table.data td.actions-col { position: sticky; z-index: 1; }
table.data tbody td.bulk-col, table.data tbody td.row-drag-col,
table.data tbody td.actions-col { background: inherit; }
table.data thead th.bulk-col, table.data thead th.row-drag-col,
table.data thead th.actions-col { z-index: 3; }
table.data th.bulk-col, table.data td.bulk-col { left: 0; }
table.data th.actions-col, table.data td.actions-col { right: 0; }
/* The handle sits behind the checkbox column, so it pins one column-width in —
   the one width in this table that is declared rather than measured. A table
   with no bulk actions has no checkbox column, and then the handle IS the
   left edge. */
table.data th.row-drag-col, table.data td.row-drag-col { left: var(--bulk-col-w); }
table.data th.row-drag-col:first-child, table.data td.row-drag-col:first-child { left: 0; }

/* The seam the scrolled-away columns disappear behind. A pseudo-element rather
   than a border (border-collapse leaves a collapsed border behind when the cell
   scrolls away from it) and rather than a box-shadow (the keyboard cursor's bar
   on the row's first cell is one, and a single element has only one). Drawn by
   whichever pinned-left column is the inner one, so the two never show a rule
   between themselves. */
table.data .bulk-col::after, table.data .row-drag-col::after, table.data .actions-col::after {
  content: ''; position: absolute; top: 0; bottom: 0; width: 1px;
  background: var(--outline-variant);
}
table.data .bulk-col::after, table.data .row-drag-col::after { right: 0; }
table.data .actions-col::after { left: 0; }
table.data tr:has(> .row-drag-col) > .bulk-col::after { content: none; }

/* Sits directly above the table and appears only once something is ticked, so an
   untouched list looks exactly as it did before. */
.bulk-bar {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: .5rem .75rem; margin-bottom: .25rem;
  border: 1px solid color-mix(in srgb, var(--primary) 35%, var(--outline-variant));
  border-radius: .5rem;
  background: color-mix(in srgb, var(--primary) 8%, var(--surface-container-low));
  font-size: 13px;
}
.bulk-bar[hidden] { display: none; }
.bulk-bar .bulk-count { font-weight: 600; color: var(--on-surface); }
.bulk-bar.busy { opacity: .6; pointer-events: none; }
/* The bulk bar's action menu is the toolbar Actions dropdown (.actions-menu), so
   it needs no styling of its own beyond the smaller button the bar calls for and
   the destructive item — Delete has to read as Delete inside a menu the way it did
   as a red button. A disabled item (one that cannot run over the whole queryset)
   dims and stops responding rather than leaving the panel. */
.bulk-bar .bulk-menu-btn { padding: .3rem .6rem; font-size: 12px; }
/* what a stopped run left behind — reads as an error without shouting, since the
   rows it names are still ticked right below it */
.bulk-bar .bulk-error { color: var(--error); font-size: 12px; }
.bulk-bar .bulk-error[hidden] { display: none; }
.actions-menu-item.danger { color: var(--error); }
.actions-menu-item.danger:hover { background: color-mix(in srgb, var(--error) 14%, transparent); }
.actions-menu-item[disabled] { opacity: .45; cursor: default; }
.actions-menu-item[disabled]:hover { background: transparent; }
/* keyboard cursor inside the panel: ↑ ↓ move focus, so focus has to be visible */
.actions-menu-item:focus-visible { background: var(--surface-container); outline: 2px solid var(--primary); outline-offset: -2px; }

/* severity-level badges (logs): info=green, warning=amber, error/critical=red */
.sev {
  display: inline-block; padding: .1rem .5rem; border-radius: 999px;
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .02em;
  border: 1px solid transparent; white-space: nowrap;
}
.sev-info { color: var(--success); background: color-mix(in srgb, var(--success) 14%, transparent); border-color: color-mix(in srgb, var(--success) 35%, transparent); }
.sev-warning { color: var(--warning); background: color-mix(in srgb, var(--warning) 16%, transparent); border-color: color-mix(in srgb, var(--warning) 38%, transparent); }
.sev-error, .sev-critical { color: var(--error); background: color-mix(in srgb, var(--error) 14%, transparent); border-color: color-mix(in srgb, var(--error) 35%, transparent); }

/* read-only detail view (log rows) */
.detail-value { font-size: 13px; color: var(--on-surface); word-break: break-word; }
.detail-value.id { font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; color: var(--on-surface-variant); }
.detail-pre {
  margin: 0; padding: .6rem .75rem;
  background: var(--surface-container); border: 1px solid var(--outline-variant);
  border-radius: .375rem; max-height: 320px; overflow: auto;
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word; color: var(--on-surface);
}
table.data .bool-icon.on { color: var(--success); }
table.data .bool-icon.off { color: var(--on-surface-variant); }

/* ---------------------------- tree ------------------------------ */
.tree { width: 100%; border-collapse: collapse; font-size: 13px; }
.tree td { padding: .4rem .6rem; border-bottom: 1px solid var(--outline-variant); }
.tree tr:hover { background: var(--surface-container-low); }
.tree .node-cell { display: flex; align-items: center; gap: .4rem; }
.tree .twisty {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: .25rem;
  cursor: pointer; color: var(--on-surface);
  border: none; background: transparent;
  flex-shrink: 0;
}
.tree .twisty svg { width: 22px; height: 22px; stroke-width: 2.5; flex-shrink: 0; }
.tree .twisty:hover { background: var(--surface-container-high); }
.tree .twisty.leaf { visibility: hidden; }
.tree .node-label { font-weight: 500; }
.tree .htmx-request .twisty svg { animation: spin .8s linear infinite; }
.kind-chip {
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  padding: .1rem .4rem; border-radius: 9999px;
  background: var(--surface-container-high); color: var(--on-surface-variant);
  font-weight: 600;
}
/* hierarchy levels — one tinted hue per level so rows scan at a glance */
.kind-chip.kind-realm      { background: color-mix(in srgb, var(--primary) 20%, transparent); color: var(--primary); }
.kind-chip.kind-domain     { background: color-mix(in srgb, var(--success) 20%, transparent); color: var(--success); }
.kind-chip.kind-rule       { background: color-mix(in srgb, var(--warning) 20%, transparent); color: var(--warning); }
.kind-chip.kind-rule_value { background: color-mix(in srgb, var(--violet)  20%, transparent); color: var(--violet); }

/* --------------------------- chips ------------------------------ */
.chip { display: inline-flex; align-items: center; gap: .3rem; font-size: 11px; padding: .15rem .5rem; border-radius: 9999px; background: var(--surface-container-high); color: var(--on-surface-variant); }
.chip.ok { background: color-mix(in srgb, var(--success) 18%, transparent); color: var(--success); }
.chip.warn { background: color-mix(in srgb, var(--warning) 18%, transparent); color: var(--warning); }
.chip.err { background: color-mix(in srgb, var(--error) 18%, transparent); color: var(--error); }
.chip.info { background: color-mix(in srgb, var(--primary) 18%, transparent); color: var(--primary); }
/* The clipboard indicator in the topbar (_shell.html): what one copy/cut is
   holding, and the way to let go of it. A chip like the user chip beside it, tinted
   like the primary actions because it is a pending one — something is waiting to be
   pasted. It is only in the DOM's flow while the buffer is full (x-show). */
.clip-chip {
  display: inline-flex; align-items: center; gap: .3rem; max-width: 15rem;
  font-size: 11px; padding: .1rem .2rem .1rem .5rem; border-radius: 9999px;
  background: color-mix(in srgb, var(--primary) 14%, transparent); color: var(--primary);
}
.clip-chip i, .clip-chip svg { width: 13px; height: 13px; flex: 0 0 auto; }
.clip-chip-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clip-chip .btn-icon { width: 18px; height: 18px; padding: 0; color: inherit; }
.clip-chip .btn-icon:hover { background: color-mix(in srgb, var(--primary) 22%, transparent); }
/* a lucide icon in a chip renders at its default 24px otherwise, dwarfing the
   11px label (affects the pagination's query/filter chips too) */
.chip svg, .chip i { width: 13px; height: 13px; flex-shrink: 0; }

/* Celery worker online/offline pill (operations queue list header). Mirrors the
   dataflow editor's .df-worker; empty until the first poll resolves. */
.worker-pill:empty { display: none; }
.worker-pill { display: inline-flex; align-items: center; gap: .35rem; font-size: 11px;
  padding: .15rem .55rem; border-radius: 9999px; white-space: nowrap;
  background: var(--surface-container-high); color: var(--on-surface-variant); }
.worker-pill .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.worker-pill.online { color: var(--success); background: color-mix(in srgb, var(--success) 16%, transparent); }
.worker-pill.offline { color: var(--warning); background: color-mix(in srgb, var(--warning) 18%, transparent); }

/* Colour-graded inline-edit select (e.g. the queue priority dropdown): tints the
   control + its options by the column's value_color_json class, reusing the chip
   palette. The tone class is set on render and refreshed on change (see
   _inline_edit.html). */
.inline-select { font-weight: 600; border-color: transparent; }
.inline-select.ok   { background: color-mix(in srgb, var(--success) 18%, transparent); color: var(--success); }
.inline-select.warn { background: color-mix(in srgb, var(--warning) 18%, transparent); color: var(--warning); }
.inline-select.err  { background: color-mix(in srgb, var(--error)   18%, transparent); color: var(--error); }
.inline-select.info { background: color-mix(in srgb, var(--primary) 18%, transparent); color: var(--primary); }
.inline-select option        { background: var(--surface-container-high); color: var(--on-surface); font-weight: 500; }
.inline-select option.ok     { color: var(--success); }
.inline-select option.warn   { color: var(--warning); }
.inline-select option.err    { color: var(--error); }
.inline-select option.info   { color: var(--primary); }

/* ------------------------- pagination --------------------------- */
.pagination { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; padding: .75rem 1.25rem; border-top: 1px solid var(--outline-variant); }
.pagination .pages { display: flex; gap: 2px; }
.pagination .page-btn { min-width: 32px; text-align: center; padding: .3rem .5rem; border-radius: .25rem; border: 1px solid var(--outline-variant); background: var(--surface-container-lowest); color: var(--on-surface); cursor: pointer; font-size: 12px; }
.pagination .page-btn.active { background: var(--primary); color: var(--on-primary); border-color: var(--primary); }
.pagination .page-btn:hover:not(.active) { background: var(--surface-container); }

/* ---------------------------- modal ----------------------------- */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: flex; align-items: flex-start; justify-content: center; padding: 4rem 1rem; z-index: 50; }
.modal { background: var(--surface-container-lowest); border: 1px solid var(--outline-variant); border-radius: .75rem; width: 100%; max-width: 640px; max-height: 85vh; overflow-y: auto; box-shadow: 0 12px 32px rgba(0,0,0,.35); }
/* Add/Edit forms: freeze the header (title + close) and footer (save) while the
   field area scrolls. The modal becomes a flex column with a non-scrolling
   shell and only the body overflowing. */
.modal-flex { display: flex; flex-direction: column; overflow: hidden; }
.modal-flex > form { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.modal-flex .card-header { flex: 0 0 auto; }
.modal-flex .card-footer { flex: 0 0 auto; }
.modal-flex .card-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

/* ------------------- maximizable modals ------------------------- */
/* Bookmarks, query builder + entity forms: 60% wide / 80% tall by default;
   .maximized on the backdrop (persisted in localStorage) grows the modal to
   nearly full screen — the backdrop padding shrinks with it. */
.modal-60 { width: 60vw; height: 80vh; max-width: none; max-height: none; }
.modal-pivot { width: 88vw; height: 84vh; max-width: 1500px; max-height: none; }
/* A modal opened *on top of* another one (the query builder's column picker).
   Sits above the backdrop below it, which stays visible and inert. */
.modal-backdrop.modal-nested { z-index: 70; background: rgba(0,0,0,.35); }
.modal-backdrop.maximized { padding: 1rem; }
.modal-backdrop.maximized .modal-60 { width: 100%; height: calc(100vh - 2rem); }
.modal-backdrop.maximized .modal-pivot { width: 100%; max-width: none; height: calc(100vh - 2rem); }

/* -------------------- full-window text editor ------------------- */
/* Every long-text field carries a small expand button (injected by
   initTextExpand in base.html) that opens its value in a window-filling
   editor — an 80px-high box inside a 60vw dialog is no way to write a
   document. It stays faint until the field is hovered or the button focused. */
.text-expand-wrap { position: relative; display: block; flex: 1 1 auto; min-width: 0; }
.text-expand-wrap > .textarea { padding-right: 1.7rem; }
/* …but a box that also offers the AI button needs room for two. */
.text-expand-wrap > .textarea[data-ai-enhance] { padding-right: 3.2rem; }
.text-expand-btn {
  position: absolute; top: .3rem; right: .3rem;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 2px; border: 0; border-radius: .25rem; background: transparent;
  color: var(--on-surface-variant); opacity: .55; cursor: pointer; line-height: 0;
}
/* In a standard field the label line above the box is free, so the button sits
   up there, next to the label it belongs to and clear of the scrollbar a text
   long enough to want this always has. Only there: a textarea laid out some
   other way (the CMS section forms) keeps it inside its own top-right corner. */
.field > .text-expand-wrap > .text-expand-btn { top: -1.4rem; right: 0; }
.field > .text-expand-wrap > .textarea { padding-right: .65rem; }
.text-expand-wrap:hover .text-expand-btn,
.text-expand-btn:focus, .text-expand-btn:hover { opacity: 1; background: var(--surface-container); color: var(--on-surface); }
.text-expand-btn svg, .text-expand-btn i { width: 15px; height: 15px; }
/* "Improve this text with AI" — the same chrome button, one slot to the left of
   the expand one, in both of the positions above. It carries the accent colour
   and sits at a higher resting opacity than its neighbour on purpose: expanding
   is a thing you go looking for, this is an offer, and an invisible offer is not
   one. */
.text-ai-btn { right: 1.65rem; opacity: .75; color: var(--primary, var(--on-surface-variant)); }
.field > .text-expand-wrap > .text-ai-btn { top: -1.4rem; right: 1.35rem; }

/* The editor itself: mounted on <body> above every dialog (a field can be
   expanded from a nested modal too), filling the window bar a small margin. */
.modal-backdrop.ag-text-editor { z-index: 85; padding: 1rem; align-items: stretch; }
.ag-text-editor > .modal { width: 100%; max-width: none; max-height: none; }
/* the body is the editor's frame — it does not scroll, the textarea inside does */
.ag-text-editor .card-body { display: flex; overflow: hidden; padding: 1rem 1.25rem; }
/* The writing surface takes the whole window — that is what opening it asked
   for. Borderless on purpose: at this size the field IS the page, and a focus
   ring around a window-filling box reads as a form control, not a document. */
.text-editor-area {
  flex: 1 1 auto; width: 100%;
  min-height: 0; resize: none; font-size: 14px; line-height: 1.65; padding: .5rem .25rem;
  border: 0; background: transparent;
}
.text-editor-area:focus { border: 0; box-shadow: none; }
/* A JSON column edits in ACE instead, filling the same frame. Absolute inside a
   relative host is what gives ACE the definite height it needs — the same
   problem initAceEditors solves with a ResizeObserver for its data-fill
   editors, which here the layout can answer on its own. */
.ag-text-editor .text-editor-ace-host { position: relative; flex: 1 1 auto; width: 100%; min-height: 0; }
.ag-text-editor .text-editor-ace { position: absolute; inset: 0; border: 0; border-radius: 0; font-size: 14px; }
/* Preview mode for a markdown field: the rendered document takes the editor's
   place in the same frame (`hidden` swaps them), and reads as a document —
   a column, not the full width the writing surface uses. The HTML arrives
   sanitized from the server, so these rules only have to style what is allowed
   through the allowlist in lib/markdown_render.py. */
.ag-text-editor .text-editor-preview {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  width: 100%; max-width: 80ch; margin: 0 auto; padding: .5rem .25rem 2rem;
}
.md-body { line-height: 1.65; }
.md-body > :first-child { margin-top: 0; }
.md-body h1, .md-body h2, .md-body h3, .md-body h4, .md-body h5, .md-body h6 {
  margin: 1.6em 0 .5em; line-height: 1.25; font-weight: 600;
}
.md-body h1 { font-size: 1.7rem; }
.md-body h2 { font-size: 1.35rem; border-bottom: 1px solid var(--outline-variant); padding-bottom: .25rem; }
.md-body h3 { font-size: 1.15rem; }
.md-body h4, .md-body h5, .md-body h6 { font-size: 1rem; }
.md-body p, .md-body ul, .md-body ol, .md-body blockquote, .md-body pre, .md-body table { margin: 0 0 1em; }
.md-body ul, .md-body ol { padding-left: 1.4em; }
.md-body li { margin: .2em 0; }
.md-body a { color: var(--primary); }
.md-body code {
  font-family: "JetBrains Mono", monospace; font-size: .88em;
  background: var(--surface-container); border-radius: .2rem; padding: .1em .3em;
}
.md-body pre {
  background: var(--surface-container); border: 1px solid var(--outline-variant);
  border-radius: .4rem; padding: .75rem 1rem; overflow-x: auto;
}
.md-body pre code { background: none; padding: 0; font-size: .85rem; }
.md-body blockquote {
  border-left: 3px solid var(--outline-variant); padding: .1rem 0 .1rem 1rem;
  color: var(--on-surface-variant);
}
.md-body hr { border: 0; border-top: 1px solid var(--outline-variant); margin: 2em 0; }
.md-body table { border-collapse: collapse; width: 100%; font-size: 13px; }
.md-body th, .md-body td { border: 1px solid var(--outline-variant); padding: .35rem .6rem; text-align: left; }
.md-body th { background: var(--surface-container); font-weight: 600; }
.md-body img { max-width: 100%; height: auto; border-radius: .3rem; }

.ag-text-editor .card-footer { justify-content: flex-start; gap: 1rem; }
.ag-text-editor .text-editor-stats { font-size: 12px; white-space: nowrap; }
/* the JSON in the editor does not parse — said where the counts are, since that
   is the line you are already glancing at */
.ag-text-editor .text-editor-stats.bad { color: var(--error); white-space: normal; }
.ag-text-editor .text-editor-hint { font-size: 11px; }
@media (max-width: 900px) { .ag-text-editor .text-editor-hint { display: none; } }

/* ------------------------- chart tooltips ----------------------- */
/* ApexCharts' light/dark tooltip can leave dark text on a dark slice/bg.
   Force the app's surface/on-surface pair (they flip with the theme), so the
   text is always readable — dark-on-light in light mode, light-on-dark in dark. */
.apexcharts-tooltip {
  background: var(--surface-container-lowest) !important;
  color: var(--on-surface) !important;
  border: 1px solid var(--outline-variant) !important;
  box-shadow: 0 4px 16px rgba(0,0,0,.25) !important;
}
.apexcharts-tooltip-title {
  background: var(--surface-container-lowest) !important;
  color: var(--on-surface) !important;
  border-bottom: 1px solid var(--outline-variant) !important;
  font-weight: 600;
}
/* every text node inside the tooltip (labels + values), overriding inline colors */
.apexcharts-tooltip * { color: var(--on-surface) !important; }

/* ---------------------------- misc ------------------------------ */
.flash { padding: .6rem .9rem; border-radius: .375rem; margin-bottom: 1rem; font-size: 13px; }
.flash.error { background: var(--error-container); color: var(--on-error-container); }
.flash.success { background: color-mix(in srgb, var(--success) 18%, transparent); color: var(--success); }

/* ---------------- a save that failed --------------------------------------
   Three things say it, and they say different halves of it: the banner counts the
   fields and lists them (each entry a way TO the field), the field wears a red
   edge so the form itself is readable as a checklist, and the tab button is marked
   so a page with nothing wrong on it does not look like the end of the work. */
.form-errors { display: flex; flex-direction: column; gap: .4rem; }
.form-errors-head { display: flex; align-items: center; gap: .4rem; font-weight: 500; }
.form-errors-head i, .form-errors-head svg { width: 15px; height: 15px; flex: 0 0 auto; }
.form-errors-list { list-style: none; margin: 0; padding: 0; display: flex;
  flex-direction: column; gap: .1rem; }
.form-error-item {
  display: flex; align-items: baseline; gap: .4rem; width: 100%; text-align: left;
  border: none; background: transparent; color: inherit; cursor: pointer;
  padding: .15rem .3rem; border-radius: .25rem; font: inherit; font-size: 12.5px;
}
.form-error-item:hover { background: color-mix(in srgb, var(--on-error-container) 10%, transparent); }
.form-error-field { font-weight: 600; flex: 0 0 auto; }
.form-error-field::after { content: ':'; }
.form-error-msg { opacity: .9; }
/* the tab bar's own marker: a dot rather than a colour swap, so the active/ghost
   state a tab already carries keeps meaning what it meant */
.btn.tab-has-error { position: relative; }
.btn.tab-has-error::after {
  content: ''; position: absolute; top: .18rem; right: .18rem;
  width: .4rem; height: .4rem; border-radius: 50%; background: var(--error);
}

/* the field itself: a red edge on whatever widget it holds, and the reason under it */
.field.has-error > .input, .field.has-error > .textarea, .field.has-error > .select,
.field.has-error input.input, .field.has-error textarea.textarea, .field.has-error select.select {
  border-color: var(--error);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--error) 45%, transparent);
}
.field.has-error > label { color: var(--error); }
.field-error {
  display: flex; align-items: center; gap: .3rem; margin: .2rem 0 0;
  font-size: 11.5px; color: var(--error);
}
.field-error i, .field-error svg { width: 12px; height: 12px; flex: 0 0 auto; }
/* one blink when the banner sends the cursor here — a field already in view would
   otherwise answer the click with nothing */
@keyframes ag-field-flash {
  0%, 100% { background: transparent; }
  30% { background: color-mix(in srgb, var(--error) 16%, transparent); }
}
.field.field-flash { animation: ag-field-flash .9s ease-out 1; border-radius: .3rem; }
.center-screen { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 1.5rem; background: var(--background); }
.auth-card { width: 100%; max-width: 400px; }
/* will-change promotes the spinner to its own compositor layer so the rotate
   animation keeps running on the GPU/compositor thread even while the main thread
   is blocked by heavy synchronous work (big JSON.parse, chart/pivot render) —
   otherwise the spinner visibly freezes exactly when it's needed most. */
.spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--outline-variant); border-top-color: var(--primary); border-radius: 50%; animation: spin .7s linear infinite; will-change: transform; }
@keyframes spin { to { transform: rotate(360deg); } }
.toolbar { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin-bottom: 1rem; }

/* ---- Frozen list layout: the heading, toolbar and column titles stay put,
   only the table body scrolls. Scoped to pages that contain a list table. ---- */
.app:has(#admin-table), .app:has(#log-table) { height: 100vh; overflow: hidden; }
.app:has(#admin-table) .content,
.app:has(#log-table) .content { min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
.app:has(#admin-table) .toolbar,
.app:has(#log-table) .toolbar { flex: 0 0 auto; }
.app:has(#admin-table) .card,
.app:has(#log-table) .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
#admin-table, #log-table { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
#admin-table > .table-wrap, #log-table > .table-wrap { flex: 1 1 auto; min-height: 0; overflow: auto; }
#admin-table > .pagination, #log-table > .pagination { flex: 0 0 auto; }
.grow { flex: 1; }
.htmx-indicator { opacity: 0; transition: opacity .2s; }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }

/* ---- Drag-to-reorder rows (initRowSort in base.html) ----
   The single reordering affordance in the UI: a grip in the row's first cell on
   any list whose table carries the standard sort_order column. There are no
   up/down arrows anywhere. */
/* The column is frozen (see "frozen columns"), so the .25rem on the right is the
   gap between the grip and the seam, not decoration. */
.row-drag-col { width: 1%; padding-left: .5rem !important; padding-right: .25rem !important; color: var(--on-surface-variant); }
.row-drag {
  display: inline-flex; align-items: center; justify-content: center;
  cursor: grab; color: var(--on-surface-variant); opacity: .55; line-height: 0;
  padding: .15rem; border-radius: .25rem;
}
.row-drag:hover { opacity: 1; background: var(--surface-container); }
.row-drag:active { cursor: grabbing; }
.row-drag svg, .row-drag i { width: 18px; height: 18px; }
/* the row being dragged — dimmed so the insertion preview reads clearly */
.row-dragging { opacity: .45; background: var(--surface-container); }

/* flag images */
.flag-img { width: 22px; height: 16px; object-fit: cover; border-radius: 2px; border: 1px solid var(--outline-variant); vertical-align: middle; flex-shrink: 0; }
/* in data-table listings, match the 24px lucide action icons; width follows
   the image's natural aspect */
table.data .flag-img { height: 24px; width: auto; object-fit: contain; }
/* align-self keeps the natural aspect inside .field (a column flexbox would
   otherwise stretch the img to full width while the height stays fixed) */
.flag-img-lg { height: 64px; width: auto; max-width: 100%; object-fit: contain; align-self: flex-start; border-radius: 3px; border: 1px solid var(--outline-variant); display: block; margin-bottom: .5rem; }

/* drag-and-drop image/file upload (see templates/_image_drop.html) */
.image-drop {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .625rem; min-height: 7rem; padding: 1rem;
  border: 1.5px dashed var(--outline-variant); border-radius: .375rem;
  background: var(--surface-container-lowest);
  transition: border-color .15s ease, background .15s ease;
}
.image-drop.dragover { border-color: var(--primary); background: color-mix(in srgb, var(--primary) 8%, transparent); }
/* native picker is hidden; the styled "Choose file" button proxies to it */
.image-drop-input { display: none; }
/* edit-box preview: never taller than 200px, aspect ratio preserved */
.image-drop-preview { max-height: 200px; max-width: 100%; width: auto; height: auto; object-fit: contain; border-radius: .25rem; border: 1px solid var(--outline-variant); }
.image-drop-prompt { display: flex; flex-direction: column; align-items: center; gap: .375rem; color: var(--on-surface-variant); font-size: 13px; text-align: center; }
.image-drop-prompt svg { width: 30px; height: 30px; stroke-width: 1.5; opacity: .85; }
.image-drop-file { display: inline-flex; align-items: center; gap: .4rem; font-size: 13px; color: var(--on-surface); }
.image-drop-file svg { width: 16px; height: 16px; }
.image-drop-actions { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; }
.image-drop-actions svg { width: 15px; height: 15px; }

/* flag-aware select widget */
.flag-select { position: relative; }
.flag-select-btn { display: flex; align-items: center; gap: .5rem; width: 100%; text-align: left; cursor: pointer; }
.flag-select-panel {
  position: absolute; z-index: 40; top: calc(100% + 4px); left: 0; right: 0;
  background: var(--surface-container-lowest); border: 1px solid var(--outline-variant);
  border-radius: .375rem; box-shadow: 0 8px 24px rgba(0,0,0,.25); padding: .4rem; max-height: 280px; display: flex; flex-direction: column; gap: .35rem;
}
/* HTMX autocomplete result panels carry this class but hold no markup until a
   search swaps results in (and are cleared again on blur/outside-click) — keep
   the empty container from rendering as a stray bordered box. */
.flag-select-panel:empty { display: none; }
.flag-select-list { overflow-y: auto; }
.flag-opt-row { display: flex; align-items: center; gap: .5rem; padding: .4rem .5rem; border-radius: .25rem; cursor: pointer; }
.flag-opt-row:hover { background: var(--surface-container); }

/* FontAwesome icon picker (iconPicker in base.html) */
.icon-select { position: relative; }
.icon-select-btn { display: flex; align-items: center; gap: .5rem; width: 100%; text-align: left; cursor: pointer; }
.icon-select-clear { display: inline-flex; cursor: pointer; color: var(--on-surface-variant); }
.icon-select-clear:hover { color: var(--on-surface); }
.icon-select-panel {
  /* full width of the field — the 5-column grid stretches to fill it */
  position: absolute; z-index: 40; top: calc(100% + 4px); left: 0; right: 0;
  background: var(--surface-container-lowest); border: 1px solid var(--outline-variant);
  border-radius: .375rem; box-shadow: 0 8px 24px rgba(0,0,0,.25); padding: .4rem;
  display: flex; flex-direction: column; gap: .35rem;
}
.icon-select-grid {
  /* a fixed row height keeps "5 rows tall" deterministic even when the panel is
     wider than its min-width; beyond 5 rows the grid scrolls. */
  overflow-y: auto; display: grid; gap: .25rem;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); grid-auto-rows: 64px;
  max-height: calc(64px * 5 + .25rem * 4);
}
.icon-opt {
  display: flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: .3rem; background: transparent;
  color: var(--on-surface); cursor: pointer; font-size: 32px;
}
.icon-opt:hover { background: var(--surface-container); border-color: var(--outline-variant); }
.icon-opt.on { background: var(--primary); color: var(--on-primary); }
/* picker footer: icon count on the left, prev/next pager on the right */
.icon-select-foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .1rem .25rem; }
.icon-select-pager { display: flex; align-items: center; gap: .15rem; }
.icon-select-pager .btn-icon:disabled { opacity: .35; cursor: default; }
/* Lucide picker cells render an injected <svg> (not a font glyph), so size the
   svg rather than font-size. */
.icon-opt-lucide svg { width: 28px; height: 28px; }
/* FontAwesome icon shown in a list cell — twice the surrounding text size */
table.data .list-icon { font-size: 2em; vertical-align: middle; }
/* Lucide icon shown in a list cell — an injected <svg>, sized explicitly */
table.data .list-icon-lucide { width: 1.6em; height: 1.6em; vertical-align: middle; }
/* JSON cell: a "braces" icon in the grid whose formatted value pops up
   underneath on hover. The popover (#json-hover-popover) is a single element
   appended to <body> by the handler in base.html so it escapes the table's
   overflow clipping; styles below cover both the trigger and that element. */
.json-cell { display: inline-flex; align-items: center; cursor: help; color: var(--on-surface-variant); }
.json-cell:hover, .json-cell:focus-visible { color: var(--primary); outline: none; }
.json-cell .json-icon { width: 18px; height: 18px; }
#json-hover-popover {
  position: fixed; z-index: 1000; display: none; pointer-events: none;
  margin: 0; max-width: 480px; max-height: 360px; overflow: hidden;
  padding: .6rem .75rem; border-radius: .5rem;
  background: var(--surface-container-high); color: var(--on-surface);
  border: 1px solid var(--outline-variant); box-shadow: 0 8px 24px rgba(0,0,0,.35);
  font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 12px; line-height: 1.45;
  white-space: pre; tab-size: 2;
}
/* Failed-status chip: the chip becomes a hover trigger revealing the row's
   error_text in the body-level #status-hover-popover (wired in base.html). */
.status-pop { cursor: help; }
#status-hover-popover {
  position: fixed; z-index: 1000; display: none; pointer-events: none;
  margin: 0; max-width: 420px; max-height: 320px; overflow: hidden;
  padding: .55rem .7rem; border-radius: .5rem;
  background: var(--surface-container-high); color: var(--on-surface);
  border: 1px solid var(--outline-variant); box-shadow: 0 8px 24px rgba(0,0,0,.35);
  font-size: 12px; line-height: 1.45; white-space: pre-wrap; word-break: break-word;
}
/* thumbnail image shown in a list cell (e.g. pickup-order photo) */
table.data .list-img { height: 44px; width: auto; max-width: 80px; object-fit: cover;
  border-radius: 4px; border: 1px solid var(--outline-variant); vertical-align: middle; }

/* --------------------------------------------------------------------------
   PivotTable.js — themed to match the app (scoped to #pivot-output, so the
   ID selectors win over the library's default class rules in either theme).
   -------------------------------------------------------------------------- */
#pivot-output { font-size: 13px; color: var(--on-surface); }
#pivot-output table.pvtUi { color: var(--on-surface); border-collapse: separate; border-spacing: 8px; }
#pivot-output table.pvtUi > tbody > tr > td { padding: 0; vertical-align: top; }

/* drop zones (unused / rows / cols / vals) */
#pivot-output .pvtAxisContainer,
#pivot-output td.pvtVals {
  background: var(--surface-container-low);
  border: 1px solid var(--outline-variant);
  border-radius: .5rem;
  padding: .4rem;
}
#pivot-output .pvtAxisContainer.pvtHorizList li,
#pivot-output .pvtAxisContainer.pvtVertList li { margin: .15rem; }

/* draggable attribute pills (look like the app's chips) */
#pivot-output .pvtAxisContainer li span.pvtAttr {
  background: var(--surface-container-highest);
  border: 1px solid var(--outline-variant);
  color: var(--on-surface);
  border-radius: 999px;
  padding: .2rem .55rem;
  font-size: 12px;
  white-space: nowrap;
  box-shadow: var(--shadow);
}
#pivot-output .pvtAxisContainer li.pvtPlaceholder {
  border: 1px dashed var(--primary);
  border-radius: 999px;
}
#pivot-output .pvtTriangle { color: var(--on-surface-variant); cursor: pointer; }

/* renderer / aggregator selectors + native selects */
#pivot-output .pvtRenderers,
#pivot-output .pvtVals { line-height: 1.8; }
#pivot-output select.pvtRenderer,
#pivot-output select.pvtAggregator,
#pivot-output select.pvtAttrDropdown,
#pivot-output .pvtVals select {
  border: 1px solid var(--outline-variant);
  border-radius: .375rem;
  background: var(--surface-container-lowest);
  color: var(--on-surface);
  padding: .25rem .45rem;
  font-size: 12px;
}
#pivot-output a.pvtRowOrder,
#pivot-output a.pvtColOrder { color: var(--on-surface-variant); text-decoration: none; }

/* the result table */
#pivot-output table.pvtTable { border-collapse: collapse; font-size: 13px; box-shadow: var(--shadow); border-radius: .5rem; overflow: hidden; }
#pivot-output table.pvtTable thead tr th,
#pivot-output table.pvtTable tbody tr th {
  background: var(--surface-container);
  color: var(--on-surface-variant);
  border: 1px solid var(--outline-variant);
  font-weight: 600;
  padding: .45rem .65rem;
  text-align: left;
}
#pivot-output table.pvtTable tbody tr td {
  background: var(--surface-container-lowest);
  color: var(--on-surface);
  border: 1px solid var(--outline-variant);
  padding: .4rem .65rem;
  text-align: right;
}
#pivot-output table.pvtTable .pvtTotalLabel,
#pivot-output table.pvtTable .pvtTotal,
#pivot-output table.pvtTable .pvtGrandTotal {
  background: var(--surface-container-high);
  color: var(--on-surface);
  font-weight: 600;
}
#pivot-output table.pvtTable tbody tr td:hover { background: var(--surface-container-low); }

/* attribute filter popup */
#pivot-output .pvtFilterBox {
  background: var(--surface-container-lowest);
  border: 1px solid var(--outline-variant);
  border-radius: .5rem;
  box-shadow: 0 12px 32px rgba(0,0,0,.25);
  color: var(--on-surface);
}
#pivot-output .pvtFilterBox h4 { color: var(--on-surface); }
#pivot-output .pvtFilterBox p { color: var(--on-surface-variant); }
#pivot-output .pvtFilterBox input[type="text"],
#pivot-output .pvtFilterBox .pvtSearch {
  border: 1px solid var(--outline-variant);
  border-radius: .375rem;
  background: var(--surface-container-lowest);
  color: var(--on-surface);
  padding: .35rem .5rem;
}
#pivot-output .pvtFilterBox .pvtCheckContainer p { border-radius: .25rem; }
#pivot-output .pvtFilterBox .pvtCheckContainer p:hover { background: var(--surface-container-low); }
#pivot-output .pvtFilterBox .pvtCheckContainer p.selected { background: var(--secondary-container); color: var(--on-secondary-container); }
#pivot-output .pvtButton {
  border: 1px solid var(--outline-variant);
  border-radius: .25rem;
  background: var(--surface-container);
  color: var(--on-surface);
  padding: .25rem .6rem;
  font-size: 12px;
  cursor: pointer;
}
#pivot-output .pvtButton:hover { background: var(--surface-container-high); }

/* Toolbar dropdowns — the export format menu (templates/_export_menu.html, used
   by the Grid popup and the manifest grid) and the list toolbar's "Actions" menu
   (templates/_actions_menu.html), which folds the analytics tools, the
   table-specific actions and those same export formats into one button. Both
   reuse the icon-picker panel look; the button anchors the panel to its right
   edge. */
.export-menu, .actions-menu { position: relative; display: inline-block; }
.export-menu-panel, .actions-menu-panel {
  position: absolute; z-index: 60; top: calc(100% + 4px); right: 0; min-width: 170px;
  background: var(--surface-container-lowest); border: 1px solid var(--outline-variant);
  border-radius: .375rem; box-shadow: 0 8px 24px rgba(0,0,0,.25); padding: .25rem;
  display: flex; flex-direction: column; gap: .1rem;
}
.export-menu-item, .actions-menu-item {
  display: flex; align-items: center; gap: .5rem; width: 100%; text-align: left;
  padding: .4rem .6rem; border: none; background: transparent; color: var(--on-surface);
  border-radius: .3rem; cursor: pointer; font-size: 13px; white-space: nowrap;
}
.export-menu-item:hover, .actions-menu-item:hover { background: var(--surface-container); text-decoration: none; }
/* menu items are <button>s, except the wizard launchers, which are links */
a.actions-menu-item { text-decoration: none; }
/* divider before the export formats (last section of the Actions menu) */
.actions-menu-sep { height: 1px; margin: .25rem .3rem; background: var(--outline-variant); }

/* ------------------------- keyboard shortcuts -------------------------- */
/* Standard chords are declared once in templates/base.html and attached to
   markup with data-shortcut="<action>"; these styles are what the user sees of
   them — the key chip on menu items and form buttons, the focused-row cursor,
   and the Alt+/ cheat-sheet. */
.sc-kbd {
  font-family: var(--font-mono, monospace); font-size: 10px; line-height: 1;
  padding: .2rem .32rem; margin-left: auto;
  border: 1px solid var(--outline-variant); border-bottom-width: 2px; border-radius: .25rem;
  background: var(--surface-container); color: var(--on-surface-variant); white-space: nowrap;
}
/* inside a footer button the chip sits next to the label, not pushed to the far
   right (that is the menu-item layout) */
.btn > .sc-kbd, .btn-sm > .sc-kbd { margin-left: .45rem; }
/* on a filled button (primary, danger) the chip borrows the button's own text
   colour instead of the page's surface tokens, which would fight the fill */
.btn:not(.btn-ghost) > .sc-kbd {
  color: inherit;
  background: color-mix(in srgb, currentColor 18%, transparent);
  border-color: color-mix(in srgb, currentColor 35%, transparent);
}

/* Row cursor — ↑/↓ move it, Enter opens the row, Del deletes it. Focusable rows
   carry tabindex (templates/table.html); the marker is a left bar plus a tint so
   it reads without relying on colour alone. */
table.data tbody tr[tabindex]:focus { outline: none; background: var(--surface-container); }
table.data tbody tr[tabindex]:focus > td:first-child { box-shadow: inset 3px 0 0 var(--primary); }
table.data tbody tr[tabindex]:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* Cheat-sheet (Alt+/) — two columns of action → chord, grouped by scope. */
.sc-help-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem 1.5rem; }
.sc-help-group h4 {
  margin: 0 0 .35rem; font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--on-surface-variant);
}
.sc-help-row {
  display: flex; align-items: center; justify-content: space-between; gap: .75rem;
  padding: .25rem 0; font-size: 13px; border-bottom: 1px solid var(--outline-variant);
}
.sc-help-row:last-child { border-bottom: none; }
.sc-help-row .sc-kbd { margin-left: 0; }
/* dimmed: the action has no target on this page */
.sc-help-row.sc-off { opacity: .45; }

/* Export chooser (templates/_export_modal.html) — the one "Export" entry in the
   Actions menu opens this instead of spilling a menu row per format. One row per
   format: icon, label + what the file is, and a download affordance on the right. */
.export-choices { display: flex; flex-direction: column; gap: .4rem; }
.export-choice {
  display: flex; align-items: center; gap: .75rem; width: 100%; text-align: left;
  padding: .6rem .75rem; border: 1px solid var(--outline-variant); border-radius: .5rem;
  background: var(--surface-container-lowest); color: var(--on-surface); cursor: pointer;
}
.export-choice:hover, .export-choice:focus-visible {
  background: var(--surface-container); border-color: var(--primary); outline: none;
}
.export-choice > span { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.export-choice strong { font-size: 13px; font-weight: 600; }
.export-choice .muted { font-size: 11px; }
.export-choice > i:first-child { width: 20px; height: 20px; flex: 0 0 auto; color: var(--primary); }
.export-choice-go { width: 16px; height: 16px; margin-left: auto; flex: 0 0 auto; color: var(--on-surface-variant); }

/* --------------------------------------------------------------------------- */
/*  Hierarchical tree + detail view (ui/tree_routes.py, templates/tree/)       */
/* --------------------------------------------------------------------------- */
/* Full-bleed two-pane layout: the content padding is dropped so the tree and
   detail panes own the whole viewport below the topbar and scroll on their own. */
.app:has(.pt-page) .content { padding: 0; }
.pt-page {
  display: grid;
  /* tree | drag handle | detail. Tree width is drag-adjustable via --pt-tree-w
     (set by the resizer JS in tree/page.html); clamped to a sane default. */
  grid-template-columns: var(--pt-tree-w, 300px) 6px 1fr;
  height: calc(100dvh - 3.25rem);   /* viewport minus the sticky topbar */
  min-height: 0;
}
.pt-resizer {
  cursor: col-resize;
  background: transparent;
  margin-left: -1px;                /* sit over the tree's right border */
  transition: background .12s ease;
}
.pt-resizer:hover, .pt-resizer.dragging { background: var(--primary); }
.pt-tree {
  border-right: 1px solid var(--outline-variant);
  background: var(--surface-container-lowest);
  display: flex; flex-direction: column; min-height: 0;
}
.pt-tree-head {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .65rem .75rem; border-bottom: 1px solid var(--outline-variant);
}
.pt-tree-title { margin: 0; font-size: 14px; display: flex; align-items: center; gap: .4rem; }
.pt-tree-title i { width: 18px; height: 18px; }
.pt-roots { flex: 1; overflow-y: auto; padding: .4rem; min-height: 0; }

/* tree node rows */
.pt-node { display: flex; flex-direction: column; }
.pt-row { display: flex; align-items: center; gap: .1rem; border-radius: .35rem; position: relative; }
.pt-row:hover { background: var(--surface-container); }
.pt-twirl {
  flex: 0 0 auto; width: 22px; height: 26px; display: inline-flex;
  align-items: center; justify-content: center; border: none; background: transparent;
  color: var(--on-surface-variant); cursor: pointer; border-radius: .3rem;
}
.pt-twirl i { width: 15px; height: 15px; transition: transform .15s ease; }
.pt-twirl.open i { transform: rotate(90deg); }
.pt-labelbtn {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: .45rem;
  border: none; background: transparent; color: var(--on-surface); cursor: pointer;
  text-align: left; padding: .32rem .4rem; border-radius: .35rem; font-size: 13px;
}
.pt-labelbtn.active { background: var(--secondary-container); color: var(--on-secondary-container); }
.pt-rootbtn { font-weight: 500; }
.pt-ico { display: inline-flex; flex: 0 0 auto; color: var(--on-surface-variant); }
.pt-ico i { width: 16px; height: 16px; }
.pt-labelbtn.active .pt-ico { color: inherit; }
.pt-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pt-kids { display: flex; flex-direction: column; padding-left: 1.15rem; gap: 1px; }

/* branch (child-tab) rows */
.pt-branch > .pt-row { padding-right: .35rem; }
.pt-branch-label { flex: 1 1 auto; font-size: 12.5px; color: var(--on-surface-variant);
  text-transform: uppercase; letter-spacing: .03em; padding: .3rem 0; }
.pt-branch-ico i { width: 15px; height: 15px; }
.pt-count {
  flex: 0 0 auto; font-size: 11px; min-width: 1.2rem; text-align: center;
  padding: .05rem .35rem; border-radius: 1rem; background: var(--surface-container-high);
  color: var(--on-surface-variant);
}

/* leaf rows */
.pt-leaf > .pt-labelbtn { padding-left: .5rem; }
.pt-dot { flex: 0 0 auto; width: 5px; height: 5px; border-radius: 50%;
  background: var(--on-surface-variant); opacity: .55; }
.pt-labelbtn.active .pt-dot { background: currentColor; opacity: 1; }
.pt-leaf-empty { font-size: 12px; padding: .3rem .5rem .3rem 1rem; }
/* drag handle on an orderable leaf: the same grip the lists use, sized for the
   tree row and kept out of sight until the row is under the pointer — its space
   is always reserved, so nothing shifts when it appears */
.pt-drag { flex: 0 0 auto; opacity: 0; padding: .1rem; }
.pt-drag i, .pt-drag svg { width: 13px; height: 13px; }
.pt-row:hover .pt-drag, .pt-node[draggable="true"] .pt-drag { opacity: .55; }
.pt-drag:hover { opacity: 1; }
/* clipboard affordances on a node row (copy / cut / paste). The cluster floats
   over the right end of the row rather than sitting in its flow, because the tree
   pane is narrow and the label is worth more than the buttons: it appears on hover
   (or whenever a paste is possible, which is the cue that the clipboard has
   somewhere to go) and covers the tail of the label while it is there. */
.pt-acts {
  position: absolute; right: .15rem; top: 50%; transform: translateY(-50%);
  display: none; align-items: center; gap: .05rem; padding: .05rem .1rem;
  border-radius: .35rem; background: var(--surface-container-high);
  box-shadow: 0 0 0 1px var(--outline-variant);
}
/* hover only reveals a cluster that HAS something to reveal (pt-acts-hover — a
   record the user may copy or cut); a paste-only cluster, on a branch or on a root,
   shows exactly when the clipboard fits it and is otherwise not there at all */
.pt-row:hover .pt-acts-hover, .pt-acts-hover:focus-within, .pt-acts.pt-acts-on { display: flex; }
.pt-act {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0; border: none; background: transparent;
  color: var(--on-surface-variant); cursor: pointer; border-radius: .25rem;
}
.pt-act:hover { color: var(--primary); background: var(--surface-container); }
.pt-act i, .pt-act svg { width: 13px; height: 13px; }
.pt-act-paste { color: var(--primary); }
/* the record currently held as a CUT — still where it was, but on its way out */
.pt-cut > .pt-row > .pt-labelbtn { opacity: .5; font-style: italic; }

/* Drag and drop across the tree (page.html's drag layer). A drag fades the record
   being moved and outlines every list that takes it — the same places its Paste
   button would have appeared — so the tree answers "where can this go?" before the
   pointer gets there. The row under the pointer fills in as the one that would take
   the drop. Rows that take nothing are left alone rather than dimmed: the marked
   ones are few, and dimming a whole tree to point at three rows reads as breakage. */
.pt-node.pt-dragging > .pt-row, tr.pt-grid-row.pt-dragging { opacity: .45; }
.pt-drop-ok {
  outline: 1px dashed color-mix(in srgb, var(--primary) 55%, transparent);
  outline-offset: -1px; border-radius: .35rem;
}
.pt-drop-active {
  outline-style: solid; outline-color: var(--primary);
  background: color-mix(in srgb, var(--primary) 15%, transparent);
}
/* the detail pane's grid is a destination too — a whole panel rather than a row, so
   it takes the marker at arm's length and tints instead of filling */
.pt-grid.pt-drop-ok { outline-offset: .5rem; border-radius: .5rem; }
.pt-grid.pt-drop-active { background: color-mix(in srgb, var(--primary) 7%, transparent); }
/* while a drag is in flight a row's own controls step out of the way, so every drag
   event lands on the element carrying the marker instead of on whichever button
   happens to be under the pointer */
.pt-dnd .pt-labelbtn, .pt-dnd .pt-twirl, .pt-dnd .pt-acts, .pt-dnd .pt-drag,
.pt-dnd .pt-grid .btn-icon { pointer-events: none; }

/* the panel head's own actions (copy / cut / delete), kept on one line */
.pt-panel-actions { display: flex; align-items: center; gap: .15rem; flex: 0 0 auto; }

.pt-add {
  display: inline-flex; align-items: center; gap: .35rem; align-self: flex-start;
  margin: .15rem 0 .3rem .5rem; padding: .25rem .5rem; border: 1px dashed var(--outline-variant);
  background: transparent; color: var(--on-surface-variant); border-radius: .35rem;
  cursor: pointer; font-size: 12px;
}
.pt-add:hover { color: var(--primary); border-color: var(--primary); }
.pt-add i { width: 14px; height: 14px; }

/* detail pane */
/* min-width:0 so a wide list scrolls INSIDE .table-wrap instead of stretching the
   grid column (a grid item's auto minimum is its content) and squeezing the tree */
.pt-detail { overflow-y: auto; min-height: 0; min-width: 0; padding: 1.25rem 1.5rem; }
.pt-panel { max-width: 760px; }
.pt-panel-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem;
  padding-bottom: .85rem; margin-bottom: 1rem; border-bottom: 1px solid var(--outline-variant);
}
.pt-panel-title { display: flex; align-items: center; gap: .6rem; min-width: 0; }
.pt-panel-title h2 { margin: 0; font-size: 18px; }
.pt-panel-title .pt-ico i { width: 22px; height: 22px; }
.pt-crumb { font-size: 12px; margin-top: .1rem; }
.pt-section-h { margin: .25rem 0 .5rem; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
.pt-form-actions { display: flex; justify-content: flex-end; gap: .5rem; padding-top: .5rem; }

/* empty placeholder */
.pt-empty {
  height: 100%; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: .6rem; text-align: center; padding: 2rem;
}
.pt-empty-ico { width: 34px; height: 34px; color: var(--on-surface-variant); opacity: .6; }

/* stack the panes on narrow screens */
@media (max-width: 760px) {
  .pt-page { grid-template-columns: 1fr; grid-template-rows: 45% 55%; height: calc(100dvh - 3.25rem); }
  .pt-tree { border-right: none; border-bottom: 1px solid var(--outline-variant); }
  .pt-resizer { display: none; }   /* panes stack vertically — no horizontal drag */
}

/* branch label is now a button (opens the list grid); keep the count right-aligned */
.pt-branch-btn { gap: .45rem; }
.pt-branch-btn .pt-branch-label { flex: 1 1 auto; }

/* branch list grid in the detail pane */
/* A list fills the pane. Only the FORM is measured (.pt-panel, 760px): a form is
   read line by line and long lines hurt it, while a grid is scanned across and
   every column capped away is one the user has to scroll to find. */
.pt-grid { max-width: none; }
.pt-grid-table { width: 100%; }
.pt-grid-row:hover { background: var(--surface-container); }
.pt-grid-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .6rem; text-align: center; padding: 2.5rem 1rem; color: var(--on-surface-variant);
  border: 1px dashed var(--outline-variant); border-radius: .5rem;
}
/* the head's New buttons — one per kind on a merged branch, so they wrap rather
   than push the title */
.pt-grid-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .35rem; }
/* Panel overview tabs (_tree_panel.html + _tree_panel_grid.html). The width follows
   the active tab: the form keeps .pt-panel's 760px line length, and only a grid tab
   widens to the whole pane — the same split .pt-grid already makes between a form
   that is read line by line and a list that is scanned across. */
.pt-panel-tabbed.pt-panel-wide { max-width: none; }
.pt-ptabs { display: flex; gap: .4rem; flex-wrap: wrap; padding: .1rem 0 .8rem; }
.pt-ptab-head { padding: 0 0 .6rem; font-size: 12px; }
/* the column-group band inside the overview: a header row, not a data row, so it
   is not hoverable and its cell spans the table */
.pt-group-head td {
  background: var(--surface-container); border-top: 1px solid var(--outline-variant);
  padding: .35rem .5rem;
}
.pt-group-btn {
  display: inline-flex; align-items: center; gap: .45rem; cursor: pointer;
  background: none; border: 0; padding: .1rem 0; font: inherit; font-weight: 500;
  color: var(--on-surface);
}
.pt-group-btn:hover { color: var(--primary); }
/* the band is a row of its own: the group on the left, its "New …" buttons pushed
   to the right, wrapping rather than squeezing the label on a narrow pane */
.pt-group-band { display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
.pt-group-adders { display: flex; flex-wrap: wrap; gap: .3rem; }
.pt-group-btn i { width: 15px; height: 15px; }
.pt-group-n { font-weight: 400; font-size: 12px; }
.pt-group-empty { padding: .5rem .75rem; font-size: 12px; }

/* which table a row of a merged branch's unified grid came from */
.pt-kind { white-space: nowrap; color: var(--on-surface-variant); }
.pt-kind i { width: 14px; height: 14px; vertical-align: -2px; margin-right: .3rem; }

/* alignment spacer where a record node has no twirl (terminal table) */
.pt-twirl-spacer { flex: 0 0 auto; width: 22px; height: 26px; display: inline-block; }

/* Match counts painted onto the tree after a search (ptApplyTally). Deliberately
   the accent colour: a node carries two numbers then — how many rows it holds
   (.pt-count) and how many of them the search hit — and only the second one is
   about what the user is doing right now. Pushed to the end of the row so the
   badges line up down the tree instead of drifting with the label width. */
.pt-hits {
  background: var(--primary); color: var(--on-primary); font-weight: 500;
  margin-left: auto;
}
/* on a branch the row count is already at the end: sit next to it, not past it */
.pt-branch-btn .pt-hits { margin-left: .25rem; }

/* The make-up of a collapsed node's total, by table ("Table manifests 9 · Column
   manifests 4 · +2 more") — on records and on branch rows alike, so the user can
   tell what is down there before opening it. It exists only for shut nodes: an
   open one shows its children, each with its own count, and repeating that would
   be noise. Driven off the twirl's own `open` class rather than any state in JS —
   the same class Alpine already toggles to rotate the chevron. */
.pt-split {
  font-size: 10.5px; line-height: 1.3; padding: 0 .4rem .25rem 2.35rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pt-node:has(> .pt-row > .pt-twirl.open) > .pt-split { display: none; }
.pt-labelbtn.active .pt-hits { background: var(--on-secondary-container); color: var(--secondary-container); }

/* Find & replace across the hierarchy (templates/tree/_tree_search*.html) ----- */
.pt-search { max-width: 900px; }
/* find and replace-with sit side by side — they are read as a pair */
.pt-search-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 .75rem; }
.pt-search-opts { display: flex; align-items: center; flex-wrap: wrap; gap: .9rem; padding: .1rem 0 .4rem; }
.pt-search-opt { display: flex; align-items: center; gap: .4rem; font-size: 13px; cursor: pointer; }
.pt-search-scope { flex: 1 1 220px; margin-left: auto; margin-bottom: 0; }
.pt-search-note { font-size: 11.5px; margin: 0 0 .4rem; }
.pt-search-summary {
  display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem;
  flex-wrap: wrap; padding: .7rem 0 .5rem; margin-top: .6rem;
  border-top: 1px solid var(--outline-variant);
}
.pt-search-actions { display: flex; gap: .4rem; }
.pt-search-none { display: flex; align-items: center; gap: .5rem; padding: 1.2rem 0; }
.pt-search-none i { width: 18px; height: 18px; }
.pt-search-group { margin-bottom: .9rem; }
/* one match = one tick box + the record it sits on, its column and the excerpt */
.pt-match {
  display: flex; align-items: flex-start; gap: .55rem; padding: .4rem .5rem;
  border-radius: .35rem;
}
.pt-match:hover { background: var(--surface-container); }
.pt-match-tick { padding-top: .15rem; }
.pt-match-body { min-width: 0; flex: 1 1 auto; }
.pt-match-head { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; }
.pt-match-name { font-weight: 500; }
.pt-match-col { font-size: 11.5px; }
.pt-match-path { font-size: 11.5px; margin-top: .05rem; }
.pt-match-snip {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px;
  margin-top: .2rem; overflow-wrap: anywhere; color: var(--on-surface-variant);
}
.pt-match-snip mark {
  background: color-mix(in srgb, var(--primary) 30%, transparent);
  color: inherit; border-radius: .15rem; padding: 0 .1rem;
}

/* Column-group tints (AGColumnGroupManifest.ui_tint) ------------------------- */
/* A table with many column groups renders as one long run of fields, and the
   group headings alone do not carry the eye — so a group may declare which of two
   alternating backgrounds it is drawn with, and the blocks become legible at a
   glance. A table with only one or two groups leaves it unset and gets no tint.

   Deliberately barely-there: the tint is mixed from the page's own surface token
   rather than a fixed colour, so it follows every theme (light, dark, and a
   generated project's palette) and never competes with a status chip or a
   validation error. "b" is the untinted slot — the alternation reads as
   tint/no-tint, which is quieter than two competing colours and degrades to
   nothing if a theme's surfaces are already close together. */
.form-section.ag-tint-a {
  background: color-mix(in srgb, var(--surface-container) 55%, transparent);
  border-radius: .4rem;
  padding: .5rem .6rem;
  /* pull the padding back out so a tinted section still lines up with an
     untinted one — only the background inset moves, not the fields */
  margin-left: -.6rem;
  margin-right: -.6rem;
}
.form-section.ag-tint-b { background: transparent; }

/* The same idea in a list: a grid that declares a band_column draws the rows
   sharing a referenced row on one background, so where one group ends and the next
   begins is visible while scrolling (templates/table.html sets the class). */
table.data > tbody > tr.ag-tint-a > td {
  background: color-mix(in srgb, var(--surface-container) 45%, transparent);
}
/* the row cursor and hover still have to win over the band */
table.data > tbody > tr.ag-tint-a:hover > td { background: var(--surface-container); }

/* The llm_log burn summary is a second .table-wrap in #log-table, and the rule
   above would treat it as a second scrolling pane: sharing the leftover height
   with the rows, then growing a vertical scrollbar over three lines of totals.

   It is a footer, so it sizes to its contents. `flex: 0 0 auto` is what removes
   the scrollbar, and the `0 0` matters in both directions: it never GROWS to fill
   space it does not need, and it never SHRINKS below its content — a shrunk box
   would overflow and the scrollbar would come straight back. The data table above
   it is the flexible one and absorbs the whole window instead.

   overflow stays `auto` on both axes rather than becoming `hidden`: it can no
   longer overflow vertically, so the y scrollbar never appears, and `hidden`
   would only mean that a summary listing an implausible number of models is
   silently cut off instead of scrolled. Horizontal scrolling is load-bearing —
   eight numeric columns do not fit a narrow window. */
#log-table > .llm-summary-wrap {
  flex: 0 0 auto; min-height: 0; overflow: auto; margin-top: .6rem;
}
/* Nothing scrolls under it, so the sticky header has nothing to stick to and
   only costs a stacking context. */
#log-table > .llm-summary-wrap table.data thead th { position: static; }

/* A form's Save/Create button shows its icon at rest and a spinner while the save
   is in flight. The two are stacked in ONE grid cell and swapped by visibility, so
   the button keeps its exact size through the swap and its label never shifts —
   and at rest the spinner costs nothing. A bare .htmx-indicator (above) only fades
   to transparent, which still reserves its 16px: that is what left Save looking
   like it was holding an icon slot it never drew. htmx marks whatever made the
   request — the button when the hx-* is on it, the form around it when it is on
   the form — and either is an ancestor of the box.
   Only those two are asked for, never a bare `.htmx-request` ancestor: a
   source-less `htmx.ajax(...)` marks document.body instead, which is an ancestor
   of every box on the page. That is how expanding a tree node — or any other
   navigation that loads through htmx.ajax — used to put a spinner on the open
   record's Save button while nothing was being saved. */
.btn-swap-box { display: inline-grid; place-items: center; }
.btn-swap-box > * { grid-area: 1 / 1; }
.btn-swap-box > .htmx-indicator { visibility: hidden; opacity: 1; }
button.htmx-request .btn-swap-box > .htmx-indicator,
form.htmx-request .btn-swap-box > .htmx-indicator { visibility: visible; }
button.htmx-request .btn-swap-box > svg,
button.htmx-request .btn-swap-box > i,
form.htmx-request .btn-swap-box > svg,
form.htmx-request .btn-swap-box > i { visibility: hidden; }
