/* ------------------------------------------------------------------ *
 *  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; }
/* 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. */
/* "Fixed width" has to be said out loud: an auto-layout table hands its leftover
   width to every column that hasn't claimed one, so the Actions column would grow
   and its right-aligned buttons drift away from the last data column — a gap that
   reads as an action that was hidden. `width: 1%` is the shrink-to-content idiom
   (the checkbox column states its width instead, because the drag handle pins at
   that exact offset), so the slack goes to the data columns where it is readable. */
table.data th.row-drag-col, table.data td.row-drag-col,
table.data th.actions-col, table.data td.actions-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); }
/* 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; }
.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; }

/* 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); }
