/* ---------------------------------------------------------------
   Design tokens — internal ops tool: dense, calm, fast to scan.
   Not a marketing surface, so restraint over flourish throughout.
   ----------------------------------------------------------------*/
:root {
  --bg: #f5f6f8;
  --surface: #ffffff;
  --border: #e2e5ea;
  --text: #1a2233;
  --text-muted: #5b6472;
  --accent: #3454d1;
  --accent-hover: #28409e;
  --accent-soft: #e8ecfb;
  --danger: #c22b2b;
  --danger-soft: #fbe9e9;
  --success: #1f9d55;
  --success-soft: #e6f6ec;

  --status-new: #6b7280;
  --status-open: #3454d1;
  --status-pending: #b3760c;
  --status-resolved: #1f9d55;
  --status-closed: #8b93a1;

  --priority-low: #8b93a1;
  --priority-medium: #3454d1;
  --priority-high: #b3760c;
  --priority-urgent: #c22b2b;

  /* Always-dark surface for the rare element (the quickadjust bar
     below) that's deliberately dark-with-light-text regardless of
     page theme -- NOT overridden in the dark-mode block below, same
     reasoning as --accent staying constant across themes. Using this
     instead of var(--text) (which used to double as this color,
     purely because --text's light-mode value happens to be dark) is
     what keeps a dark-mode page from ever flipping this to a
     near-white background under white text. */
  --surface-inverse: #1a2233;

  --radius: 6px;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "Roboto Mono", Menlo, Consolas, monospace;
  /* Both overridden per-request by base.html's <head> (see
     core.context_processors.theme_settings) from the signed-in user's
     own accounts.UserPreference, if they're allowed one and have set
     it, else core.models.CustomizationSettings' org-wide default --
     these two hardcoded values are only what anyone loading this file
     outside that template (or before the override <style> block
     parses) actually gets. Every font-size in this file is written as
     calc(Npx * var(--font-scale, 1)) rather than a plain px value so
     this one variable rescales all of them together. */
  --font-scale: 1;
}

/* Dark mode -- toggled by templates/base.html's theme-toggle button,
   which sets/reads this attribute on <html> (persisted in
   localStorage, applied by a blocking inline script before first
   paint so there's no flash of the wrong theme). --accent/
   --accent-hover/--accent-soft/--accent-contrast are deliberately NOT
   overridden here: those come from the admin-configured accent color
   (see base.html's <head>) and stay the same color in both themes --
   only the neutrals (background/surface/border/text) flip. */
html[data-theme="dark"] {
  --bg: #14161c;
  --surface: #1c1f27;
  --border: #333844;
  --text: #e7e9ee;
  --text-muted: #9aa1b0;
  --danger: #e0554f;
  --danger-soft: #3a2223;
  --success: #35b56a;
  --success-soft: #16321f;

  --status-new: #9aa1b0;
  --status-open: #6d8bff;
  --status-pending: #e0a63a;
  --status-resolved: #35b56a;
  --status-closed: #6b7280;

  --priority-low: #9aa1b0;
  --priority-medium: #6d8bff;
  --priority-high: #e0a63a;
  --priority-urgent: #e0554f;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font-ui);
  background: var(--bg);
  color: var(--text);
  font-size: calc(14px * var(--font-scale, 1));
  line-height: 1.5;
}

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

/* --- Top nav --- */
.topnav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  height: 56px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.topnav__brand {
  font-weight: 700;
  font-size: calc(15px * var(--font-scale, 1));
  color: var(--text);
  letter-spacing: -0.01em;
}
.topnav__links { display: flex; gap: 20px; align-items: center; }
.topnav__links a { color: var(--text-muted); font-weight: 500; }
.topnav__links a:hover { color: var(--text); text-decoration: none; }
.topnav__links a.is-active { color: var(--accent); }
/* .topnav__links a's color rule above is more specific than a bare
   .btn-primary (two classes vs. one), so without this the "New ticket"
   / "Log in" nav buttons lose their contrasting text to the muted gray
   meant for plain nav links -- illegible against the button's
   accent-colored background. --accent-contrast (see base.html's <head>)
   is whichever of black/white actually contrasts with the
   admin-configured accent color, not a hardcoded white. */
.topnav__links .btn-primary,
.topnav__links .btn-primary:hover { color: var(--accent-contrast, #fff); }

/* Fixed footprint regardless of which glyph (sun/moon) is showing --
   without this, switching themes changes the button's rendered width
   (glyphs have different advance widths in most fonts), which visibly
   shifts every nav item after it left/right by a couple pixels. */
#theme-toggle {
  width: 32px;
  padding: 4px 0;
  justify-content: center;
  flex-shrink: 0;
}

/* --- Account menu: first-name trigger (hover for full name/username/
   email, see the title attribute in base.html) + click-to-open panel
   of account actions -- same <details>/<summary> dropdown pattern as
   .search-multiselect/.columns-customize above, just anchored to the
   right edge since this is the last item in the top nav. Closing on
   an outside click is handled by base.html's own small inline script
   (this one needs to work on every page, not just the ticket queue
   where ticket-queue.js's version of the same behavior lives). */
.account-menu { position: relative; }
.account-menu__summary {
  display: flex; align-items: center; gap: 4px;
  list-style: none; cursor: pointer; user-select: none;
  padding: 6px 10px; border-radius: var(--radius);
  font-size: calc(13px * var(--font-scale, 1)); font-weight: 600; color: var(--text);
  max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.account-menu__summary:hover { background: var(--bg); }
.account-menu__summary::-webkit-details-marker { display: none; }
.account-menu__summary::after { content: " \25BE"; opacity: 0.6; }
.account-menu[open] > .account-menu__summary { background: var(--bg); }
.account-menu__panel {
  position: absolute; z-index: 30; top: calc(100% + 4px); right: 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 6px; min-width: 220px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.12);
  display: flex; flex-direction: column; gap: 1px;
}
.account-menu__meta {
  padding: 6px 10px 8px; margin-bottom: 4px; border-bottom: 1px solid var(--border);
  font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted);
  overflow-wrap: break-word;
}
.account-menu__item {
  display: block; width: 100%; text-align: left;
  padding: 7px 10px; border-radius: 4px; border: none; background: none;
  font-family: inherit; font-size: calc(13px * var(--font-scale, 1)); font-weight: 500; color: var(--text);
  cursor: pointer;
}
.account-menu__item:hover { background: var(--bg); text-decoration: none; }
.account-menu__divider { height: 1px; background: var(--border); margin: 4px 2px; }

/* --- App layout: sidebar (Admin section only) + main content --- */
.app-layout { display: flex; align-items: flex-start; gap: 32px; }
.app-main {
  flex: 1 1 0%;
  min-width: 0;
  max-width: 1100px;
  margin: 0 auto;
  padding: 28px 24px 60px;
}
.app-sidebar {
  flex: 0 0 200px;
  padding: 28px 0 60px 24px;
}
.app-sidebar__title { font-weight: 700; font-size: calc(13px * var(--font-scale, 1)); text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); margin-bottom: 12px; }
.app-sidebar__group-title { font-weight: 700; font-size: calc(11px * var(--font-scale, 1)); text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); margin: 14px 0 4px; }
.app-sidebar__link {
  display: block;
  padding: 7px 10px;
  border-radius: var(--radius);
  font-size: calc(13px * var(--font-scale, 1));
  color: var(--text);
  margin-bottom: 2px;
}
.app-sidebar__link:hover { background: var(--bg); text-decoration: none; }
.app-sidebar__link.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 700; }
.app-sidebar__link--nested { padding-left: 20px; }

h1 { font-size: calc(20px * var(--font-scale, 1)); margin: 0 0 4px; letter-spacing: -0.01em; }
h2 { font-size: calc(16px * var(--font-scale, 1)); margin: 0 0 12px; }
.page-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 20px;
}
.page-subtitle { color: var(--text-muted); font-size: calc(13px * var(--font-scale, 1)); }

/* --- Buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: calc(13px * var(--font-scale, 1));
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
}
.btn:hover { border-color: var(--text-muted); text-decoration: none; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast, #fff); }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn-danger { color: var(--danger); }
.btn-sm { padding: 4px 10px; font-size: calc(12px * var(--font-scale, 1)); }

/* --- Cards / tables --- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
}
table { width: 100%; border-collapse: collapse; }
th {
  text-align: left;
  font-size: calc(11px * var(--font-scale, 1));
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}
td { padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: calc(13px * var(--font-scale, 1)); vertical-align: middle; }
tr:last-child td { border-bottom: none; }
/* Every table in the app shares this one wrapper (see
   static/js/table-scroll.js's own docstring) -- capping it at 60vh and
   letting both axes scroll means a table with lots of rows or a wide,
   column-heavy queue (ticket list, reports' "mixed" fallback table)
   never forces the whole page to stretch or clips content off the
   right edge; it gets its own small scrollable viewport instead, drag-
   scrollable with the mouse the same way a touchscreen already
   scrolls it with a finger. Harmless for a short table -- max-height
   only kicks in once content actually exceeds it, no visible change
   otherwise. thead stays pinned to the top of that viewport while the
   body scrolls underneath it. */
.table-wrap {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  overflow: auto; max-height: 60vh; cursor: grab;
}
.table-wrap.is-dragging { cursor: grabbing; user-select: none; }
.table-wrap thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
.row-link { color: var(--text); font-weight: 600; }

/* --- Ticket description hover preview (ticket queue/list + reports
   tables) -- see static/js/ticket-preview.js for the show/hide/position
   logic and tickets.templatetags.ticket_extras.ticket_preview for how
   the description text going into data-ticket-preview is built. */
/* cursor:help (the old value here) renders an OS-drawn "?" badge next
   to the pointer in most browsers -- redundant and distracting next to
   our own custom tooltip (ticket-preview.js). pointer (not default) --
   the ticket queue table's trigger is a real link to the ticket, and
   the reports table's trigger (a plain span) gets the same hover-for-
   detail affordance either way, so one cursor value reads correctly
   for both. */
.ticket-preview-trigger { cursor: pointer; text-decoration-style: dotted; }
.ticket-preview-tooltip {
  display: none;
  position: fixed;
  z-index: 300;
  /* Capped in both dimensions on purpose -- a long description should
     scroll inside the box rather than stretch it off the edge of the
     screen. */
  max-width: 360px;
  max-height: 280px;
  overflow-y: auto;
  padding: 10px 12px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
  font-size: calc(13px * var(--font-scale, 1));
  line-height: 1.45;
  white-space: pre-wrap;
  word-wrap: break-word;
  pointer-events: none;
}
.ticket-preview-tooltip.is-visible { display: block; }

/* --- Badges --- */
.badge {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: calc(11px * var(--font-scale, 1));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: #fff;
}
.badge-status-new { background: var(--status-new); }
.badge-status-open { background: var(--status-open); }
.badge-status-pending { background: var(--status-pending); }
.badge-status-resolved { background: var(--status-resolved); }
.badge-status-closed { background: var(--status-closed); }
.badge-status-cancelled { background: var(--danger); }

.badge-priority-low { background: var(--priority-low); }
.badge-priority-medium { background: var(--priority-medium); }
.badge-priority-high { background: var(--priority-high); }
.badge-priority-urgent { background: var(--priority-urgent); }

.badge-status-in_use { background: var(--success); }
.badge-status-in_stock { background: var(--accent); color: var(--accent-contrast, #fff); }
.badge-status-in_repair { background: var(--status-pending); }
.badge-status-retired { background: var(--status-closed); }
.badge-status-lost { background: var(--danger); }
.badge-status-danger { background: var(--danger); }

.badge-loaner { background: var(--status-pending); margin-left: 6px; }

/* --- SLA status badges (ticket detail + ticket queue) --- */
.sla-badge { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: calc(11px * var(--font-scale, 1)); font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em; }
.sla-badge--met { background: var(--success-soft); color: var(--success); }
.sla-badge--breached { background: var(--danger-soft); color: var(--danger); }
.sla-badge--overdue { background: var(--danger-soft); color: var(--danger); }
.sla-badge--pending { background: var(--accent-soft); color: var(--accent); }

.pill { font-family: var(--font-mono); color: var(--text-muted); font-size: calc(12px * var(--font-scale, 1)); }

/* --- Forms --- */
label { display: block; font-size: calc(12px * var(--font-scale, 1)); font-weight: 600; color: var(--text-muted); margin: 14px 0 4px; }
label:first-child { margin-top: 0; }
input[type=text], input[type=email], input[type=password], input[type=search],
textarea, select {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: inherit;
  font-size: calc(13px * var(--font-scale, 1));
  background: var(--surface);
  color: var(--text);
}
textarea { resize: vertical; }
.field-error { color: var(--danger); font-size: calc(12px * var(--font-scale, 1)); margin-top: 4px; }
.helptext { color: var(--text-muted); font-size: calc(12px * var(--font-scale, 1)); margin-top: 4px; }
.checkbox-row { display: flex; align-items: center; gap: 8px; margin: 14px 0 4px; }
.checkbox-row input { width: auto; }
.checkbox-row label { margin: 0; }

/* --- Combobox dropdown (static/js/combobox.js) -----------------------
   Wraps every ".typeahead-input"/"[data-combobox-remote]" field in a
   positioned <span> at enhancement time so its suggestion list can sit
   absolutely underneath it -- see that file's own docstring for why
   this replaced the old native <input list> + <datalist> popup (no way
   to make Tab/Enter reliably commit a highlighted suggestion in one).
   Used on ticket detail's status/type/priority/assigned agent/team/
   requester/department/asset/Cc fields, the asset checkout borrower
   field, and the asset Quick Assign screen's user search alike -- one
   shared look for every field of this kind, anywhere in the app. */
.combobox { position: relative; display: inline-block; width: 100%; }
.combobox__list {
  position: absolute; z-index: 40; top: calc(100% + 2px); left: 0; right: 0;
  margin: 0; padding: 4px; list-style: none;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  max-height: 260px; overflow-y: auto;
}
.combobox__option {
  display: flex; flex-direction: column; gap: 1px;
  padding: 6px 8px; border-radius: 4px; cursor: pointer;
  font-size: calc(13px * var(--font-scale, 1)); color: var(--text);
}
.combobox__option-sub { font-size: calc(11px * var(--font-scale, 1)); color: var(--text-muted); }
.combobox__option.is-active, .combobox__option:hover { background: var(--accent-soft); }
.combobox__option.is-active .combobox__option-sub, .combobox__option:hover .combobox__option-sub { color: inherit; }
.combobox__empty { padding: 6px 8px; font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); }

/* --filter-item-min-width/--filter-search-min-width default to each
   field's normal comfortable minimum (150px / 220px) -- static/js/
   ticket-queue.js narrows them (down to a floor, never below what's
   legible) only when the row would otherwise wrap to a second line,
   the same "shrink before wrap" idea as .ticket-meta's fields on the
   ticket detail page (see static/js/ticket-meta-layout.js). */
.filter-bar { display: flex; gap: 12px; margin-bottom: 16px; align-items: flex-end; flex-wrap: wrap; }
.filter-bar label { margin: 0 0 4px; }
/* Only the free-text search box and the search-multiselect dropdowns
   are meant to shrink -- .filter-bar__actions (Filter/Clear) and
   .filter-bar__more (More filters) size to their own button/label
   content regardless, since narrowing a button's clickable label
   helps no one (see static/js/ticket-queue.js's layoutFilterBar). */
.filter-bar > .search-multiselect { min-width: var(--filter-item-min-width, 150px); }
/* flex-basis:auto (not a fixed px basis) so the rendered/wrap-decision
   width actually tracks min-width as it's narrowed -- a numeric basis
   here would out-rank min-width and never visibly shrink, the bug
   that shipped once already (see layoutFilterBar in ticket-queue.js). */
.filter-bar__search { flex: 1 1 auto; min-width: var(--filter-search-min-width, 220px); }
.filter-bar__more { flex-basis: 100%; margin-top: 4px; }
.filter-bar__more summary { cursor: pointer; font-size: calc(13px * var(--font-scale, 1)); color: var(--text-muted); font-weight: 600; }
.filter-bar__more-grid { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 10px; }
.filter-bar__more-grid > div { min-width: 150px; }
.filter-bar__more-grid label { display: block; margin: 0 0 4px; font-size: calc(13px * var(--font-scale, 1)); }
.filter-bar__columns-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 6px; margin-top: 10px; max-width: 720px; }

/* --- Messages --- */
.message {
  padding: 10px 14px;
  border-radius: var(--radius);
  margin-bottom: 16px;
  font-size: calc(13px * var(--font-scale, 1));
  font-weight: 500;
}
.message-success { background: var(--success-soft); color: var(--success); }
.message-error { background: var(--danger-soft); color: var(--danger); }

/* --- Ticket detail ------------------------------------------------------
   Fields are a fixed, known-size CSS Grid rather than 1fr columns that
   stretch to fill the row -- 1fr would keep each field's width tied to
   however wide .app-main happens to be (absurdly wide fields on a big
   monitor), and would leave no unused space for justify-content to
   center into when there are fewer fields than a full row. minmax's
   140-200px range is the "narrow within reason" floor/ceiling; auto-fit
   collapses unused tracks so a short row (e.g. the 1-2 field SLA block
   below) centers instead of hugging the left edge. This still can't
   shrink an *already-committed* row of tracks below 200px before
   wrapping the next field to a second row (CSS Grid has no such
   mechanism) -- static/js/ticket-meta-layout.js is what actually
   forces every field pictured together onto one row, computing and
   setting an explicit (not auto-fit) grid-template-columns once it
   knows exactly how many fields there are and how much room they have;
   this rule is only what renders before that script runs, and the
   fallback for anyone with JS disabled. */
.ticket-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 200px)); justify-content: center; gap: 14px; margin: 16px 0 24px; }
/* min-width: 0 -- a grid item's automatic minimum width is "auto"
   (its content's min-content size) by default, not 0. Without this, a
   <select>/<input> inside .value (whose own min-content is a UA-
   default ~170-200px regardless of its own width:100%) would keep
   forcing this item wider than its track no matter how narrow the
   track itself is allowed to get, and the field would still overflow
   its cell -- the other half of the ticket-meta--fields mobile fix
   below (its minmax(0, 1fr) tracks are the first half). */
.ticket-meta__item { min-width: 0; }
.ticket-meta__item .label { font-size: calc(11px * var(--font-scale, 1)); text-transform: uppercase; color: var(--text-muted); letter-spacing: 0.03em; margin-bottom: 2px; }
.ticket-meta__item .value { font-size: calc(13px * var(--font-scale, 1)); font-weight: 600; }

/* .ticket-meta--fields -- the ticket detail page's own main field grid
   (Status/Priority/Requester/.../Type). An additional class alongside
   the shared .ticket-meta above, not a replacement for it: .ticket-meta
   is also reused, unmodified, by reservation/asset/workflow/password/
   user-settings detail pages and by this same page's own SLA sub-block
   further down, none of which want a fixed 5/2-column layout or the
   field-specific reordering below. static/js/ticket-meta-layout.js
   is scoped to skip this class for the same reason -- its dynamic
   column-width JS would otherwise fight the fixed layout here.
   Exactly 2 rows of 5 fields wide; on mobile (or a thin desktop
   window, <=900px) it drops to 2 columns / 5 rows instead -- see the
   @media block below for the field order that makes that reflow land
   where it's supposed to. Tighter gap/margin than the
   shared .ticket-meta rule above: a known, fixed field layout doesn't
   need as much breathing room as a variable-count auto-fit one to
   still read cleanly. */
/* minmax(0, 1fr), not a bare 1fr: a bare 1fr track's implicit minimum
   is auto (the content's min-content size), and a <select>/<input>'s
   own UA-default min-content width (~170-200px) is wider than these
   columns ever get on a narrow viewport -- that mismatch is what was
   pushing fields off screen on mobile. minmax(0, 1fr) drops the track
   floor to 0 so the already-width:100% field controls can actually
   shrink to fit the column instead of blowing out past it. */
.ticket-meta--fields { grid-template-columns: repeat(5, minmax(0, 1fr)); justify-content: stretch; gap: 8px 16px; margin: 8px 0 14px; }
@media (max-width: 900px) {
    /* 2 columns / 5 rows -- narrow enough (mobile, or a thin desktop
       window) that even the 4-wide step down still doesn't leave each
       field enough room, so this collapses straight to 2-wide instead
       of adding a third step. */
    .ticket-meta--fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    /* DOM order alone already produces the 5-wide layout above (see
       ticket_detail.html), so only this narrower layout needs explicit
       ordering -- every field gets one so a field left at the default
       order:0 doesn't jump ahead of ones given an explicit value.
       Paired by relation so each row reads as one row-of-2 in the DOM's
       own grouping (status+priority, requester+department, ccd+tags,
       asset+agent, team+type). */
    .ticket-meta--fields .ticket-meta__item--status { order: 1; }
    .ticket-meta--fields .ticket-meta__item--priority { order: 2; }
    .ticket-meta--fields .ticket-meta__item--requester { order: 3; }
    .ticket-meta--fields .ticket-meta__item--department { order: 4; }
    .ticket-meta--fields .ticket-meta__item--ccd { order: 5; }
    .ticket-meta--fields .ticket-meta__item--tags { order: 6; }
    .ticket-meta--fields .ticket-meta__item--asset { order: 7; }
    .ticket-meta--fields .ticket-meta__item--agent { order: 8; }
    .ticket-meta--fields .ticket-meta__item--team { order: 9; }
    .ticket-meta--fields .ticket-meta__item--type { order: 10; }
}

.comment { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; margin-bottom: 10px; background: var(--surface); }
.comment.is-internal { background: #fff9e8; border-color: #eddca0; }
.comment__meta { font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); margin-bottom: 6px; display: flex; gap: 8px; align-items: center; }
.comment__body { white-space: pre-wrap; }

.activity { font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); padding: 6px 0; border-bottom: 1px dashed var(--border); }
.activity:last-child { border-bottom: none; }

.section { margin-top: 28px; }
.empty-state { color: var(--text-muted); font-size: calc(13px * var(--font-scale, 1)); padding: 24px; text-align: center; }

.status-form { display: inline-flex; gap: 8px; align-items: center; }

/* --- Report bar chart (server-rendered, no JS chart lib needed) --- */
.bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.bar-row__label { width: 160px; font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); text-align: right; flex-shrink: 0; }
.bar-row__track { flex: 1; background: var(--bg); border-radius: 4px; overflow: hidden; height: 22px; }
.bar-row__fill { background: var(--accent); height: 100%; border-radius: 4px; display: flex; align-items: center; }
.bar-row__count { font-size: calc(11px * var(--font-scale, 1)); color: var(--accent-contrast, #fff); padding-left: 8px; font-weight: 700; white-space: nowrap; }
.bar-row__count.outside { color: var(--text); padding-left: 4px; }

/* --- Portrait (vertical column) bar chart -- same data/colors as the
   landscape .bar-row variant above, just rotated 90deg conceptually
   rather than sharing markup, since flex-direction alone can't flip
   "fill toward the label" from width to height cleanly. --- */
.bar-columns { display: flex; align-items: flex-end; gap: 14px; height: 260px; padding-top: 10px; }
.bar-column { display: flex; flex-direction: column; align-items: center; flex: 1; height: 100%; min-width: 36px; }
.bar-column__track { flex: 1; width: 100%; max-width: 56px; background: var(--bg); border-radius: 4px; overflow: hidden; display: flex; align-items: flex-end; }
.bar-column__fill { background: var(--accent); width: 100%; border-radius: 4px 4px 0 0; display: flex; align-items: flex-start; justify-content: center; }
.bar-column__count { font-size: calc(11px * var(--font-scale, 1)); color: var(--accent-contrast, #fff); padding-top: 4px; font-weight: 700; white-space: nowrap; }
.bar-column__label { font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); text-align: center; margin-top: 8px; word-break: break-word; }

/* --- Ticket list bulk-select quick-adjust bar --- */
.quickadjust-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* translateY(100%) is relative to the element's own (rendered)
     height, unlike a fixed `bottom: -100px`, which only fully hid the
     bar when it happened to wrap onto exactly the number of lines that
     offset was tuned for -- a wider set of quickadjust-field selects
     wrapping onto an extra line (any narrower/odd-aspect-ratio window)
     made the bar taller than the guessed offset and left a sliver
     visible along the bottom edge. This hides it completely no matter
     how tall it renders. visibility+pointer-events are the second half
     of "fully invisible": off-screen alone still leaves it focusable/
     hit-testable, which matters when nothing is selected.
  */
  transform: translateY(100%);
  visibility: hidden;
  pointer-events: none;
  background: var(--surface-inverse);
  color: #fff;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.15);
  padding: 14px 24px;
  z-index: 50;
  transition: transform 0.15s ease-out, visibility 0s linear 0.15s;
}
.quickadjust-bar--visible {
  transform: translateY(0);
  visibility: visible;
  pointer-events: auto;
  transition: transform 0.15s ease-out, visibility 0s linear 0s;
}
.quickadjust-inner { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 14px; max-width: 1400px; margin: 0 auto; }
.quickadjust-count { font-weight: 700; font-size: calc(13px * var(--font-scale, 1)); padding-bottom: 8px; white-space: nowrap; }
.quickadjust-field { display: flex; flex-direction: column; gap: 4px; font-size: calc(11px * var(--font-scale, 1)); color: #d3d7de; text-transform: uppercase; letter-spacing: 0.03em; }
.quickadjust-field select {
  font-size: calc(13px * var(--font-scale, 1));
  padding: 6px 8px;
  border-radius: var(--radius);
  border: 1px solid #4a5265;
  background: #2a3142;
  color: #fff;
  min-width: 140px;
}
.quickadjust-actions { display: flex; gap: 8px; margin-left: auto; padding-bottom: 1px; }
/* Plain (non-primary) buttons here -- "Clear selection" -- keep .btn's
   own white background, so they need .btn's own dark text too. Only
   .btn-primary ("Apply to selected") sits on a navy/accent background
   and needs white text. Previously both got color:#fff here, which
   made "Clear selection" white-on-white and unreadable. */
.quickadjust-actions .btn { border-color: #4a5265; }
.quickadjust-actions .btn-primary { background: var(--accent); border-color: var(--accent); }

/* --- Knowledge Base --- */
.kb-layout { display: flex; gap: 24px; align-items: flex-start; }
.kb-sidebar { flex: 0 0 200px; }
.kb-sidebar__card { padding: 12px; }
.kb-sidebar__title { font-weight: 700; font-size: calc(11px * var(--font-scale, 1)); text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); margin-bottom: 8px; }
.kb-main { flex: 1; min-width: 0; }

.kb-category-link { display: block; padding: 6px 8px; border-radius: var(--radius); font-size: calc(13px * var(--font-scale, 1)); color: var(--text); margin-bottom: 2px; }
.kb-category-link:hover { background: var(--bg); text-decoration: none; }
.kb-category-link--active { background: var(--accent-soft); color: var(--accent); font-weight: 700; }

.kb-article-row { margin-bottom: 12px; }
.kb-article-row__title { font-size: calc(16px * var(--font-scale, 1)); font-weight: 700; }

.kb-form-card { max-width: 900px; }

/* Default typography for rendered article HTML -- authors can paste
   from Word/Google Docs or hand-edit markup, and this makes either
   look reasonable without per-article styling. */
.kb-article-body { line-height: 1.6; }
.kb-article-body h1, .kb-article-body h2, .kb-article-body h3,
.kb-article-body h4, .kb-article-body h5, .kb-article-body h6 {
  margin: 1.2em 0 0.5em; line-height: 1.3;
}
.kb-article-body h1:first-child, .kb-article-body h2:first-child, .kb-article-body h3:first-child { margin-top: 0; }
.kb-article-body p { margin: 0 0 1em; }
.kb-article-body ul, .kb-article-body ol { margin: 0 0 1em; padding-left: 1.5em; }
.kb-article-body li { margin-bottom: 0.3em; }
.kb-article-body blockquote { margin: 0 0 1em; padding: 8px 16px; border-left: 3px solid var(--accent); background: var(--bg); color: var(--text-muted); }
.kb-article-body pre { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; overflow-x: auto; font-family: var(--font-mono); font-size: calc(13px * var(--font-scale, 1)); }
.kb-article-body code { font-family: var(--font-mono); font-size: 0.9em; background: var(--bg); padding: 1px 5px; border-radius: 4px; }
.kb-article-body pre code { background: none; padding: 0; }
.kb-article-body img, .kb-article-body iframe, .kb-article-body video { max-width: 100%; border-radius: var(--radius); }
.kb-article-body table { border-collapse: collapse; width: 100%; margin: 0 0 1em; }
.kb-article-body th, .kb-article-body td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
.kb-article-body th { background: var(--bg); }
.kb-article-body hr { border: none; border-top: 1px solid var(--border); margin: 1.5em 0; }
.kb-article-body a { text-decoration: underline; }

/* --- Knowledge Base editor --- */
.kb-editor { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin-bottom: 4px; }
.kb-editor__toolbar { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px; background: var(--bg); border-bottom: 1px solid var(--border); }
.kb-editor__btn { padding: 5px 10px; font-size: calc(12px * var(--font-scale, 1)); border: 1px solid var(--border); border-radius: 4px; background: var(--surface); color: var(--text); cursor: pointer; }
.kb-editor__btn:hover { border-color: var(--text-muted); }
.kb-editor__btn--source { margin-left: auto; font-weight: 600; }
.kb-editor__surface {
  min-height: 320px;
  padding: 16px;
  background: var(--surface);
  outline: none;
  line-height: 1.6;
}
.kb-editor__surface:empty::before { content: "Start writing, or paste from Word/Google Docs..."; color: var(--text-muted); }
.kb-editor-source { width: 100%; min-height: 320px; font-family: var(--font-mono); font-size: calc(13px * var(--font-scale, 1)); padding: 16px; border: none; border-top: 1px solid var(--border); }

/* --- KB inline password placeholder / embed / insert modal --- */
.kb-password-placeholder {
  display: inline-block;
  border: 1px dashed var(--text-muted);
  border-radius: var(--radius);
  padding: 8px 12px;
  color: var(--text-muted);
  background: var(--bg);
  cursor: pointer;
}
.kb-password-placeholder:hover { border-color: var(--accent, #3B82F6); color: var(--text); }
.kb-password-embed {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 12px;
  background: var(--bg);
}
.kb-pw-modal-backdrop {
  position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4);
  display: flex; align-items: center; justify-content: center;
  z-index: 1000;
}
.kb-pw-modal {
  background: var(--surface); border-radius: var(--radius); padding: 20px;
  width: 100%; max-width: 420px; max-height: 80vh; overflow-y: auto;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.kb-pw-modal h3 { margin-top: 0; }
.kb-pw-modal__list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.kb-pw-modal__item {
  text-align: left; padding: 8px 10px; border: 1px solid var(--border); border-radius: 4px;
  background: var(--surface); color: var(--text); cursor: pointer;
}
.kb-pw-modal__item:hover { border-color: var(--accent, #3B82F6); }
.kb-pw-modal__actions { display: flex; justify-content: space-between; gap: 8px; }

/* --- Ticket description/comment rich text editor --- */
.rt-editor { border: 1px solid var(--border); border-radius: var(--radius); overflow: visible; margin-bottom: 4px; }
.rt-editor .kb-editor__surface { min-height: 100px; padding: 10px 12px; line-height: 1.5; }
.rt-editor .rich-text-source { width: 100%; min-height: 100px; font-family: var(--font-mono); font-size: calc(13px * var(--font-scale, 1)); padding: 10px 12px; border: none; border-top: 1px solid var(--border); }
.rt-editor .kb-editor__surface img { max-width: 100%; }
/* A ticket-description-sized editor (see ticket_detail.html) gets a
   taller starting surface than a comment box, matching how large the
   plain textareas each replaced already were. */
.ticket-description-input.kb-editor__surface,
.rt-editor:has(.ticket-description-input) .kb-editor__surface { min-height: 200px; }

.rt-editor__resize-box { position: absolute; pointer-events: none; }
.rt-editor__resize-handle {
  position: absolute; right: -5px; bottom: -5px; width: 10px; height: 10px;
  background: var(--accent); border: 1px solid #fff; border-radius: 2px;
  cursor: nwse-resize; pointer-events: auto;
}

/* --- Ticket attachments --- */
.attachment-list { display: flex; flex-direction: column; gap: 10px; }
.attachment-list--comment { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--border); }

.attachment-item { display: flex; align-items: center; gap: 12px; padding: 8px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.attachment-item__thumb-link { flex-shrink: 0; position: relative; display: block; cursor: zoom-in; }
.attachment-item__thumb { width: 48px; height: 48px; object-fit: cover; border-radius: 4px; display: block; }
.attachment-item__expand-hint {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.35); color: #fff; font-size: calc(14px * var(--font-scale, 1)); border-radius: 4px;
  opacity: 0; transition: opacity 0.1s ease;
}
.attachment-item__thumb-link:hover .attachment-item__expand-hint { opacity: 1; }
.attachment-item__newtab { display: block; font-size: calc(11px * var(--font-scale, 1)); color: var(--text-muted); margin-top: 2px; }
.attachment-item__icon {
  width: 48px; height: 48px; flex-shrink: 0; border-radius: 4px;
  background: var(--bg); color: var(--text-muted);
  display: flex; align-items: center; justify-content: center;
  font-size: calc(10px * var(--font-scale, 1)); font-weight: 700; letter-spacing: 0.02em;
}
.attachment-item__info { flex: 1; min-width: 0; }
.attachment-item__info a { font-weight: 600; word-break: break-word; }
.attachment-item__remove { margin-left: auto; flex-shrink: 0; }

/* --- Photo lightbox (in-page expand/zoom for image attachments) --- */
.photo-lightbox {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(10, 12, 16, 0.92);
  display: flex; flex-direction: column;
  visibility: hidden; opacity: 0; transition: opacity 0.12s ease, visibility 0s linear 0.12s;
}
.photo-lightbox--open { visibility: visible; opacity: 1; transition: opacity 0.12s ease, visibility 0s linear 0s; }
.photo-lightbox__bar {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 16px; flex-shrink: 0;
}
.photo-lightbox__title { color: #fff; font-size: calc(13px * var(--font-scale, 1)); font-weight: 600; word-break: break-word; }
.photo-lightbox__controls { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.photo-lightbox__zoom-label { color: #fff; font-size: calc(12px * var(--font-scale, 1)); min-width: 40px; text-align: center; }
.photo-lightbox__viewport {
  flex: 1; overflow: auto; display: flex; align-items: center; justify-content: center;
  cursor: zoom-out; padding: 16px;
}
.photo-lightbox__img { max-width: 100%; max-height: 100%; transition: transform 0.12s ease; cursor: default; }
.photo-lightbox__img--zoomed { max-width: none; max-height: none; cursor: grab; }

/* File-picker feedback (attachment-picker.js) */
.attachment-picker__list { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; }
.attachment-picker__item { font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); }

/* --- View original email --- */
.original-email__headers {
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 12px 14px; font-family: var(--font-mono); font-size: calc(12px * var(--font-scale, 1)); white-space: pre-wrap; word-break: break-word;
}
.original-email__text {
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 14px; font-family: var(--font-mono); font-size: calc(13px * var(--font-scale, 1)); white-space: pre-wrap; word-break: break-word;
}
.original-email__frame {
  width: 100%; min-height: 500px; border: 1px solid var(--border); border-radius: var(--radius); background: #fff;
}

/* --- Reporting: builder + results (Phase 10 revamp) ------------------ */

.report-builder-grid {
  display: grid;
  grid-template-columns: 380px 1fr;
  gap: 24px;
  align-items: start;
}
@media (max-width: 900px) {
  .report-builder-grid { grid-template-columns: 1fr; }
}

.checkbox-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 10px;
  margin: 6px 0 14px;
}

.small-label { font-size: calc(11px * var(--font-scale, 1)); color: var(--text-muted); margin: 0 0 2px; }

.filter-row {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin-bottom: 10px;
  background: var(--bg);
}
.filter-row--pinned { background: var(--accent-soft); border-color: var(--accent); }
.filter-row__top { display: flex; align-items: center; justify-content: space-between; }
.filter-row__label { font-size: calc(12px * var(--font-scale, 1)); font-weight: 600; color: var(--text); }
.filter-row__control { margin-top: 6px; }
.filter-row__control select,
.filter-row__control input[type="text"] { width: 100%; }
.filter-row__remove {
  border: none; background: none; color: var(--text-muted); cursor: pointer;
  font-size: calc(16px * var(--font-scale, 1)); line-height: 1; padding: 0 2px;
}
.filter-row__remove:hover { color: var(--danger); }
.filter-row__checks { display: flex; flex-direction: column; gap: 4px; max-height: 160px; overflow-y: auto; }
.filter-row__checks .checkbox-row { margin: 0; }

.custom-date-range { display: none; gap: 8px; margin-top: 8px; }
.custom-date-range > div { flex: 1; }

.add-filter-bar { margin: 4px 0 18px; }
.add-filter-bar select { width: 100%; }

.badge-object-type { background: var(--text-muted); }

.report-number { font-size: calc(56px * var(--font-scale, 1)); font-weight: 700; color: var(--accent); line-height: 1; }

.spline-chart { width: 100%; height: auto; max-width: 680px; }
.spline-chart__label { font-size: calc(9px * var(--font-scale, 1)); fill: var(--text-muted); }
.spline-chart__value { font-size: calc(10px * var(--font-scale, 1)); fill: var(--text); font-weight: 600; }

.pie-chart-wrap { display: flex; align-items: center; gap: 28px; flex-wrap: wrap; }
.pie-chart { width: 180px; height: 180px; border-radius: 50%; flex-shrink: 0; }
.pie-legend { display: flex; flex-direction: column; gap: 6px; }
.pie-legend__row { display: flex; align-items: center; gap: 8px; font-size: calc(13px * var(--font-scale, 1)); }
.pie-legend__swatch { width: 12px; height: 12px; border-radius: 3px; flex-shrink: 0; }
.pie-legend__label { min-width: 120px; }
.pie-legend__count { color: var(--text-muted); font-size: calc(12px * var(--font-scale, 1)); }

/* --- Dashboard: customizable pinned-report widgets --------------------- */

.dashboard-widget-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
@media (max-width: 900px) {
  .dashboard-widget-grid { grid-template-columns: 1fr; }
  /* Same shrink-below-content-width fix as .app-main/.kb-main above,
     CSS Grid's version of it: a grid item defaults to `min-width:
     auto` (its content's min-content size) same as a flex item, so a
     dashboard widget containing a wide table/report would otherwise
     still force this single 1fr column -- and the page along with it
     -- wider than the viewport even though the grid itself collapsed
     to one column. */
  .dashboard-widget { min-width: 0; }
}
.dashboard-widget { padding: 14px 16px; }
.dashboard-widget__header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border);
  font-weight: 600;
}
.dashboard-widget__actions { display: flex; gap: 4px; }
.dashboard-widget__actions form { display: inline; }
.dashboard-widget__actions .btn-sm { padding: 2px 8px; }
.dashboard-widget__body { overflow: auto; }

/* --- Live-updating queues / reports ------------------------------------ */

.live-indicator {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: calc(11px * var(--font-scale, 1)); text-transform: uppercase; letter-spacing: .04em;
  color: var(--success);
}
.live-indicator::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--success); display: inline-block;
  animation: live-pulse 2s ease-in-out infinite;
}
@keyframes live-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}

.message-info { background: var(--accent-soft); color: var(--accent); }

/* --- Customer portal --------------------------------------------------- */

.portal-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  align-items: start;
}
@media (max-width: 900px) {
  .portal-grid { grid-template-columns: 1fr; }
}
.portal-section { padding: 24px; display: flex; flex-direction: column; }
.portal-section h2 { margin: 0 0 6px; }
.portal-section__cta { margin-top: 16px; align-self: flex-start; }
.portal-form-list { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; }
.portal-form-list__item { text-align: left; justify-content: flex-start; }

/* --- Ticket queue: full-width layout ----------------------------------
   .app-main caps at 1100px by default (good for most pages), but the
   ticket queue's filter bar has enough fields that it wraps onto a
   second row well before that -- .app-main--wide (set via
   ticket_list.html's main_class block) raises the cap so everything
   fits on one row on a normal desktop viewport. margin:auto (inherited
   from .app-main) still centers the block itself once the viewport is
   wider than the cap; .filter-bar's own justify-content below centers
   the *fields* within that block the same way whenever they don't need
   the full row. */
.app-main--wide { max-width: 1800px; }
.app-main--wide .filter-bar { justify-content: center; }

/* --- Ticket queue: generic text-searchable multi-select filter --------
   Shared by every filter field on the ticket queue (status, priority,
   type, source, team, department, tags, assigned agent, requester,
   asset) -- see tickets/templatetags/ticket_extras.py's
   search_multiselect tag and _search_multiselect.html. A native
   checkbox list under a search box, collapsed behind <details> so it
   reads as one compact control per field instead of a wall of
   checkboxes. */
.search-multiselect { position: relative; display: flex; flex-direction: column; }
.search-multiselect__details { position: relative; }
.search-multiselect__summary {
  display: block; list-style: none; cursor: pointer; user-select: none;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius);
  font-size: calc(13px * var(--font-scale, 1)); min-width: var(--filter-item-min-width, 150px); background: var(--surface);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.search-multiselect__summary::-webkit-details-marker { display: none; }
.search-multiselect__summary::after { content: " \25BE"; opacity: 0.6; float: right; }
.search-multiselect__details[open] > .search-multiselect__summary { border-color: var(--accent); }
.search-multiselect__panel {
  position: absolute; z-index: 30; top: calc(100% + 4px); left: 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px; min-width: 240px; max-width: 320px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.12);
  display: flex; flex-direction: column; gap: 6px;
}
.search-multiselect__search {
  padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px;
  font-size: calc(12px * var(--font-scale, 1)); background: var(--bg); color: var(--text);
}
.search-multiselect__options { max-height: 240px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.search-multiselect__option { margin: 0 !important; padding: 4px 2px; font-size: calc(13px * var(--font-scale, 1)); font-weight: 400; }
.search-multiselect__empty { font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); padding: 4px 2px; }

/* --tags: the ticket detail page's "TAGS" field (see ticket_detail.html)
   repurposes the summary line itself to show the ticket's *already
   applied* tags as colored pills (with their own remove buttons)
   instead of the default "N selected"/placeholder summary text --
   that's what saves the separate badges-then-picker two-line layout a
   pill each. static/js/search-multiselect.js's data-static-summary
   guard is what stops syncSummary() from clobbering this with its own
   text. */
.search-multiselect__summary--tags {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px; min-height: 26px;
  white-space: normal; overflow: visible; text-overflow: clip; padding: 4px 8px;
}
.search-multiselect__summary--tags::after { margin-left: auto; align-self: center; }
.search-multiselect__placeholder { color: var(--text-muted); font-size: calc(13px * var(--font-scale, 1)); }

/* "Clear all" -- resets the picker's own in-progress checkbox
   selection (the tags about to be added/submitted), not whatever's
   already applied/saved elsewhere on the page. static/js/
   search-multiselect.js shows this only once at least one checkbox in
   the same panel is checked, and hides it again once none are. */
.search-multiselect__footer { display: flex; align-items: center; gap: 10px; margin-top: 2px; }
.search-multiselect__clear {
  background: none; border: none; padding: 0; cursor: pointer;
  font-size: calc(12px * var(--font-scale, 1)); color: var(--text-muted); text-decoration: underline;
}
.search-multiselect__clear:hover { color: var(--text); }

/* --- Ticket queue: "More filters" as an inline dropdown, not a
   full-width row -- moved into the filter bar itself between Tags and
   the Filter button, so it no longer forces its own row the way
   flex-basis:100% used to. Only tickets/ticket_list.html uses this
   class, so it's safe to restyle outright rather than add a modifier. */
.filter-bar__more { position: relative; flex-basis: auto; margin-top: 0; align-self: flex-end; }
.filter-bar__more summary {
  list-style: none; cursor: pointer; user-select: none;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius);
  font-size: calc(13px * var(--font-scale, 1)); font-weight: 600; color: var(--text); background: var(--surface);
  white-space: nowrap;
}
.filter-bar__more summary::-webkit-details-marker { display: none; }
.filter-bar__more summary::after { content: " \25BE"; opacity: 0.6; }
.filter-bar__more[open] > summary { border-color: var(--accent); }
.filter-bar__more-grid {
  position: absolute; z-index: 25; top: calc(100% + 4px); left: 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.12);
  display: flex; flex-wrap: wrap; gap: 12px; margin-top: 0;
  width: max-content; max-width: min(720px, 90vw);
}
.filter-bar__more-grid > div { min-width: 150px; }

/* --- Ticket queue: columns customizer, tucked into the table's
   top-right corner instead of its own full-width row above it. --- */
.ticket-table-region { position: relative; padding-top: 44px; }
.columns-customize { position: absolute; top: 0; right: 0; z-index: 10; }
.columns-customize > summary {
  list-style: none; cursor: pointer; user-select: none;
  padding: 6px 12px; border: 1px solid var(--border); border-radius: var(--radius);
  font-size: calc(12px * var(--font-scale, 1)); font-weight: 600; color: var(--text-muted); background: var(--surface);
}
.columns-customize > summary::-webkit-details-marker { display: none; }
.columns-customize > summary::after { content: " \25BE"; opacity: 0.6; }
.columns-customize[open] > summary { border-color: var(--accent); color: var(--text); }
.columns-customize__panel {
  position: absolute; z-index: 20; top: calc(100% + 4px); right: 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 14px 16px; min-width: 280px; max-width: 90vw;
  box-shadow: 0 4px 16px rgba(0,0,0,0.12);
}

@media (max-width: 900px) {
  .app-main--wide { max-width: 100%; }
  .app-main--wide .filter-bar { justify-content: flex-start; }
  .filter-bar__more-grid { position: static; box-shadow: none; border: none; padding: 10px 0 0; width: auto; max-width: none; }
  .ticket-table-region { padding-top: 40px; }
}

/* --- Ticket queue: Kanban board (templates/tickets/ticket_kanban.html,
   static/js/ticket-kanban.js) --- */
.kanban-board {
  display: flex; gap: 12px; align-items: flex-start;
  overflow-x: auto; padding-bottom: 12px; margin-top: 16px;
}
.kanban-column {
  flex: 0 0 260px; width: 260px;
  background: var(--bg-muted, var(--surface)); border: 1px solid var(--border); border-radius: var(--radius);
  display: flex; flex-direction: column; max-height: calc(100vh - 320px); min-height: 200px;
}
.kanban-column__header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 12px; border-bottom: 1px solid var(--border);
  position: sticky; top: 0; background: inherit; border-radius: var(--radius) var(--radius) 0 0;
}
.kanban-column__count {
  font-size: calc(12px * var(--font-scale, 1)); font-weight: 700; color: var(--text-muted);
  background: var(--surface); border: 1px solid var(--border); border-radius: 999px;
  min-width: 22px; text-align: center; padding: 1px 6px;
}
.kanban-column__cards {
  flex: 1; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 8px;
  transition: background-color 0.1s ease;
}
.kanban-column__cards--dragover { background: var(--danger-soft, rgba(52,84,209,0.08)); }
.kanban-column__empty {
  color: var(--text-muted); font-size: calc(12px * var(--font-scale, 1)); text-align: center; padding: 16px 4px;
}
.kanban-card {
  display: block; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 10px; text-decoration: none; color: var(--text); cursor: grab;
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
.kanban-card:hover { border-color: var(--accent); }
.kanban-card--dragging { opacity: 0.4; }
.kanban-card__id { font-size: calc(11px * var(--font-scale, 1)); color: var(--text-muted); margin-bottom: 2px; }
.kanban-card__subject {
  font-size: calc(13px * var(--font-scale, 1)); font-weight: 600; margin-bottom: 8px;
  overflow-wrap: break-word;
}
.kanban-card__meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.kanban-card__tags { margin-top: 6px; }
.kanban-column__more {
  display: block; text-align: center; font-size: calc(12px * var(--font-scale, 1));
  color: var(--accent); padding: 6px; text-decoration: none;
}
.kanban-column__more:hover { text-decoration: underline; }

@media (max-width: 900px) {
  .kanban-column { flex: 0 0 220px; width: 220px; }
}

/* --- Top nav: pinned to the top of the screen on mobile -------------
   Desktop is untouched (Troy's "keep desktop exactly as it looks
   today" rule, see the block comment below) -- .topnav there stays a
   normal in-flow block that scrolls away with the page, same as ever.
   Below 900px it becomes `position: sticky; top: 0`, so it stays
   pinned to the top of the viewport as the page scrolls instead of
   disappearing upward with the rest of the content. z-index: 80 sits
   above everything that renders inline in the page (the highest
   otherwise in use is the mobile hamburger dropdown's own 60, which
   is a descendant of .topnav anyway) but below the modal/lightbox/
   tooltip overlays (200-1000) that are meant to cover the whole
   screen, nav included, while they're open. */
@media (max-width: 900px) {
  .topnav { position: sticky; top: 0; z-index: 80; }
}

/* ============================================================
   Mobile revamp -- Troy asked to keep desktop exactly as it looks
   today, so every rule below that changes how something renders lives
   inside a `@media (max-width: 900px)` block (the app's one existing
   responsive cutoff -- see the handful of other max-width:900px rules
   earlier in this file, e.g. .ticket-meta--fields, .dashboard-widget-
   grid, .report-builder-grid). The few unqualified (all-width) rules
   mixed in below only set up a *new* element (the hamburger toggle,
   the little category-edit button, sticky/scrollable table styling)
   in a state that's either identical to the old look or an explicit,
   requested exception -- never a desktop layout change.
   ============================================================ */

/* --- Any page's header row (Assets, Knowledge Base, reports, etc.) --
   Stack title-block above button-row and let the button row wrap onto
   as many lines as it needs, instead of squeezing/overflowing
   horizontally. This alone is what turns "Assets" + "N assets" +
   [Classroom sets][Parts inventory][Quick assign][New asset] into the
   requested "title on its own line, buttons wrap below it" layout --
   see .ticket-detail-header below for ticket detail's extra reordering
   on top of this same baseline. */
@media (max-width: 900px) {
  .page-header { flex-direction: column; align-items: stretch; gap: 10px; }
  .page-header > div:last-child { flex-wrap: wrap; }
}

/* --- Ticket detail header: "Ticket # and subject" / buttons / "opened
   at X" as three distinct rows, in that order, instead of the desktop
   two-column (title block containing both the subject AND "opened at"
   line, buttons off to the right) layout. The title block's own
   wrapper becomes `display: contents` -- it stops generating its own
   box and its children (the <h1>, any subject field-errors, and the
   "opened at" .page-subtitle) become direct flex items of
   .ticket-detail-header itself, right alongside the button row, so
   `order` can interleave all of them into the requested sequence
   without touching the template's DOM structure (and therefore
   without touching how it renders above 900px, where display:contents
   never applies). */
@media (max-width: 900px) {
  .ticket-detail-header > div:first-child { display: contents; }
  .ticket-detail-header h1 { order: 1; }
  .ticket-detail-header .field-error { order: 1; }
  .ticket-detail-header > div:last-child { order: 2; }
  .ticket-detail-header .page-subtitle { order: 3; }
}

/* --- Report bar charts trailing off the right edge -----------------
   Landscape (.bar-row): the label column's fixed 160px was eating too
   much of a narrow phone's width, leaving barely any room for the bar
   itself -- shrink it instead of leaving it fixed.
   Portrait (.bar-columns): a flex row with no wrap and no scroll, so
   more than a handful of groups already overflowed the card's edge on
   a narrow screen with nowhere for the overflow to go -- letting it
   scroll horizontally (same drag-to-scroll affordance as .table-wrap,
   see static/js/table-scroll.js, which enhances anything matching
   ".table-wrap, .bar-columns") means every bar stays reachable instead
   of being clipped. */
@media (max-width: 900px) {
  .bar-row__label { width: 90px; }
  .bar-columns { overflow-x: auto; cursor: grab; }
  .bar-columns.is-dragging { cursor: grabbing; user-select: none; }
}

/* --- Admin sidebar / Knowledge Base categories sidebar --------------
   Both stack full-width above the main content instead of staying a
   fixed-width column squeezing it. `align-items: stretch` is the fix
   for a real bug this surfaced: .app-layout/.kb-layout's own
   `align-items: flex-start` (desktop default, so the sidebar isn't
   stretched to match a tall main column) only constrains the CROSS
   axis -- which is vertical in the normal row layout, but becomes
   HORIZONTAL once flex-direction flips to column here. Without this
   override, `flex-start` on the cross axis makes each child shrink-
   to-fit its own content width instead of filling the 375px-ish
   viewport, so .app-main (which has `min-width: 0` for exactly this
   kind of shrinking, but that alone isn't enough once align-items
   stops stretching it) balloons out to match its widest descendant
   (e.g. a table) and the *entire page* scrolls horizontally --
   defeating the point of .table-wrap's own contained scroll/drag
   area above. Stretching keeps every child pinned to the full
   available width so only .table-wrap (or .bar-columns) ever
   overflows, never the page itself. */
@media (max-width: 900px) {
  .app-layout, .kb-layout { flex-direction: column; align-items: stretch; gap: 16px; }
  .app-sidebar, .kb-sidebar { flex: 1 1 auto; width: 100%; padding: 0; }
  /* Belt-and-suspenders alongside align-items:stretch above -- a wide
     .table-wrap descendant can still drag .app-main/.kb-main's own box
     wider than the viewport even when stretched, because the browser's
     cross-axis stretch sizing still yields to a content-based
     automatic minimum size once that content (a table) is wider than
     the stretched size. Pinning an explicit 100%/border-box width here
     removes that content-based sizing entirely, so .app-main's box
     itself can never exceed the viewport -- only .table-wrap (or
     .bar-columns) scrolls internally past that point, same as
     intended above. */
  .app-main, .kb-main { width: 100%; max-width: 100%; box-sizing: border-box; }
}

/* --- Collapsible sidebar/category headings (base.html's
   .app-sidebar-collapse, article_list.html's .kb-sidebar-collapse) --
   Both wrap what used to be a plain, static <div> title in a
   <details>/<summary> so they can become a collapsed-by-default
   "tap to expand" button on mobile without a second copy of the link
   list markup. Above 900px the summary is restyled back to look like
   that original plain heading (no disclosure arrow, default cursor).
   Staying expanded there comes from the `open` attribute being on by
   default in the template (only collapsed via a small inline script,
   see collapseIfMobile in base.html's <head> and its call sites) --
   NOT from the `display: block !important` below. That override used
   to be the only mechanism and looked like it worked by inspecting
   the rendered HTML source, but it doesn't actually work: Chromium
   hides a closed <details>'s non-<summary> content at the shadow-DOM
   level, which no author CSS on the light-DOM child can override, so
   a closed-by-default panel stayed genuinely empty on desktop no
   matter what `display` said. It's left below as a harmless no-op
   fallback (it only matches `:not([open])`, which desktop should
   never actually be) rather than a real safety net. */
.app-sidebar__title--toggle, .kb-sidebar__title--toggle {
  display: flex; align-items: center; gap: 6px; cursor: default;
}
.app-sidebar__title--toggle::-webkit-details-marker,
.kb-sidebar__title--toggle::-webkit-details-marker { display: none; }
@media (min-width: 901px) {
  .app-sidebar-collapse:not([open]) > .app-sidebar-collapse__body,
  .kb-sidebar-collapse:not([open]) > .kb-sidebar-collapse__body { display: block !important; }
}
@media (max-width: 900px) {
  .app-sidebar__title--toggle, .kb-sidebar__title--toggle { cursor: pointer; }
  .app-sidebar__title--toggle:hover, .kb-sidebar__title--toggle:hover { color: var(--accent); }
  .app-sidebar__title--toggle::after, .kb-sidebar__title--toggle::after {
    content: "\25B8"; margin-left: auto; font-size: 12px; transition: transform 0.15s ease;
  }
  details[open] > .app-sidebar__title--toggle::after,
  details[open] > .kb-sidebar__title--toggle::after { transform: rotate(90deg); }
  .app-sidebar-collapse__body, .kb-sidebar-collapse__body { margin-top: 10px; }
}
/* Little "manage categories" edit button next to the Categories
   heading -- both desktop and mobile per Troy's explicit ask, unlike
   everything else in this section. */
.kb-sidebar__edit-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 4px; flex-shrink: 0;
  color: var(--text-muted); font-size: 13px; line-height: 1; cursor: pointer;
}
.kb-sidebar__edit-btn:hover { background: var(--bg); color: var(--text); text-decoration: none; }

/* --- Topnav hamburger menu (base.html's new .topnav-menu wrapping the
   existing .topnav__links) -------------------------------------------
   Above 900px this is inert: the toggle stays hidden, and .topnav-menu
   is `open` by default in the template (see collapseIfMobile in
   base.html's <head>) and guarded from ever toggling closed there
   (static/js/mobile-nav.js's "toggle" listener snaps it back open if
   it somehow isn't) -- so .topnav__links looks and behaves exactly
   like the plain, non-collapsible row it always was. Below 900px the
   inline collapseIfMobile script removes `open` before first paint,
   and the details' native open/closed toggling takes back over,
   turning that same content into a right-anchored dropdown panel
   instead of an always-visible row -- "put a menu icon at the top
   right ... vertical list" from the ask. */
.topnav-menu { position: static; }
/* Hidden by default -- a divider only makes sense once .topnav__links
   becomes a stacked mobile list; in the normal desktop flex row it'd
   just show up as a stray sliver between items. */
.topnav-menu__divider { display: none; }
.topnav-menu__toggle {
  display: none; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: var(--radius); cursor: pointer; list-style: none;
}
.topnav-menu__toggle::-webkit-details-marker { display: none; }
.hamburger-icon { display: flex; flex-direction: column; gap: 4px; width: 18px; }
.hamburger-icon span { display: block; height: 2px; background: var(--text); border-radius: 1px; }
@media (min-width: 901px) {
  .topnav-menu:not([open]) > .topnav__links { display: flex !important; }
}
@media (max-width: 900px) {
  .topnav-menu { position: relative; }
  .topnav-menu__toggle { display: flex; }
  .topnav-menu__toggle:hover { background: var(--bg); }
  .topnav-menu:not([open]) > .topnav__links { display: none; }
  .topnav-menu[open] > .topnav__links {
    position: absolute; top: calc(100% + 6px); right: 0; z-index: 60;
    display: flex; flex-direction: column; align-items: stretch; gap: 2px;
    background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
    padding: 8px; min-width: 220px; max-width: calc(100vw - 16px);
    max-height: calc(100vh - 76px); overflow-y: auto;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  }
  .topnav-menu[open] > .topnav__links > a,
  .topnav-menu[open] > .topnav__links > button {
    width: 100%; justify-content: flex-start; box-sizing: border-box;
  }
  /* :not(.btn) so this generic row styling doesn't fight the "New ticket" /
     "Log in" buttons' own .btn-primary treatment -- those should keep
     looking like buttons even inside the mobile menu panel. */
  .topnav-menu[open] > .topnav__links > a:not(.btn) { padding: 9px 10px; border-radius: 4px; color: var(--text); }
  .topnav-menu[open] > .topnav__links > a:not(.btn):hover { background: var(--bg); text-decoration: none; }
  .topnav-menu[open] > .topnav__links > a.is-active:not(.btn) { background: var(--accent-soft); color: var(--accent); }
  .topnav-menu[open] > .topnav__links > a.btn { margin: 2px 0; }
  .topnav-menu[open] > .topnav__links > hr.topnav-menu__divider {
    display: block; width: 100%; border: none; border-top: 1px solid var(--border); margin: 4px 0;
  }
  /* The nested account-menu (user's name trigger + its own Admin/
     Reservations/etc panel) is the "submenu" from the ask -- it should
     "show off to the left" of the panel it's nested in: flush against
     the trigger's own left edge (right: 100%) rather than below it, so
     it visibly cascades away from the hamburger icon in the corner
     instead of stacking straight down and getting cramped. If there
     isn't enough room to its left on a very narrow phone,
     static/js/mobile-nav.js clamps it back on screen rather than
     letting it clip off the edge. */
  .topnav-menu[open] .account-menu { width: 100%; }
  .topnav-menu[open] .account-menu__summary { width: 100%; max-width: none; box-sizing: border-box; }
  /* Base/fallback position (flush left of the trigger) for the instant
     between [open] being set and static/js/mobile-nav.js's toggle
     handler repositioning it -- static/js/mobile-nav.js immediately
     switches this to `position: fixed` with explicit inline top/left
     (see its own docstring) because the hamburger panel this lives
     inside (.topnav-menu[open] > .topnav__links, just above) scrolls
     with `overflow-y: auto` when the menu is tall -- an absolutely
     positioned descendant stays clipped to that scroll box no matter
     how it's offset, which is exactly the "don't let things clip off
     screen" case Troy called out. Viewport-relative fixed positioning,
     computed in JS from the trigger's actual on-screen position,
     escapes that clipping entirely. */
  .topnav-menu[open] .account-menu__panel { top: 0; right: 100%; left: auto; margin-right: 6px; }
}

/* ============================================================
   Workflow flowchart canvas -- see templates/workflows/workflow_edit.html
   and static/js/workflow-canvas.js. */
.workflow-canvas-wrap { display: flex; gap: 16px; align-items: flex-start; }
.workflow-canvas-palette {
  display: flex; flex-direction: column; gap: 8px; flex: 0 0 180px;
  position: sticky; top: 76px;
}
.workflow-canvas-palette__item {
  background: var(--surface); border: 1px dashed var(--border); border-radius: 6px;
  padding: 10px 12px; font-size: 13px; cursor: grab; user-select: none;
}
.workflow-canvas-palette__item:hover { border-color: var(--accent); color: var(--accent); }
.workflow-canvas {
  position: relative; flex: 1; min-height: 420px; border: 1px solid var(--border);
  border-radius: 8px; background: var(--bg);
  background-image: radial-gradient(var(--border) 1px, transparent 1px);
  background-size: 16px 16px; overflow: auto;
}
.workflow-canvas__edges { position: absolute; top: 0; left: 0; pointer-events: none; overflow: visible; }
.workflow-edge { fill: none; stroke: var(--text-muted); stroke-width: 2; pointer-events: none; }
.workflow-edge--conditioned { stroke-dasharray: 6 4; stroke: var(--accent); }
.workflow-edge--pending { stroke: var(--accent); stroke-dasharray: 4 4; pointer-events: none; }
/* The actual hover/interaction target for a connector -- invisible and
   much fatter than the 2px line drawn above it, since a 2px target is
   too thin to hover reliably. Swaps the visible line's color on
   hover purely via a CSS sibling-adjacent selector so JS never has to
   touch the visible path's own style. */
.workflow-edge__hit { fill: none; stroke: transparent; stroke-width: 16; pointer-events: stroke; cursor: pointer; }
.workflow-edge__hit:hover + .workflow-edge { stroke: var(--accent); }
.workflow-node {
  position: absolute; width: 190px; background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px; padding: 10px 12px; cursor: grab; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.workflow-node--trigger { border-color: var(--accent); background: var(--accent-soft); }
.workflow-node__header { font-size: 11px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
.workflow-node__name { font-weight: 600; margin-top: 2px; word-break: break-word; }
.workflow-node__actions { display: flex; gap: 6px; margin-top: 8px; }
.workflow-node__port {
  position: absolute; top: 50%; width: 12px; height: 12px; margin-top: -6px;
  border-radius: 50%; background: var(--surface); border: 2px solid var(--accent); cursor: crosshair;
}
.workflow-node__port--in { left: -7px; }
.workflow-node__port--out { right: -7px; }
.workflow-node--complete { border-color: var(--accent); }
/* Same backdrop/modal shape as .kb-pw-modal-backdrop above -- shown by
   templates/workflows/workflow_edit.html's inline script when a
   "Can't activate this workflow yet" message comes back from the
   server (see workflows/views/builder.py::workflow_edit and
   services.workflow_validation_errors), so the exact list of what
   needs fixing is impossible to miss. */
.workflow-validation-modal-backdrop {
  position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4);
  display: flex; align-items: center; justify-content: center;
  z-index: 6000;
}
.workflow-validation-modal {
  background: var(--surface); border-radius: var(--radius); padding: 20px;
  width: 100%; max-width: 480px; max-height: 80vh; overflow-y: auto;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.workflow-validation-modal h3 { margin-top: 0; }
.workflow-validation-modal ul { padding-left: 20px; margin: 12px 0; }
.workflow-validation-modal li { margin-bottom: 6px; }
/* Appended straight to <body>, not the canvas -- see
   static/js/workflow-canvas.js's showEdgePopup/getEdgePopup for why
   ("always on top", never clipped by the canvas's own overflow). Fixed
   positioning + a z-index well above every other z-index in this
   stylesheet (the highest anywhere else is 1000) is what actually
   delivers "always clickable when it pops up." */
.workflow-edge-popup {
  position: fixed; z-index: 5000; display: flex; gap: 6px; padding: 6px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.18);
}
