/* ============================================================================
   MiniCRM design system
   ----------------------------------------------------------------------------
   Layers, in order: tokens -> reset -> layout shell -> components -> utilities.
   Three rules hold the whole thing together:
     1. Nothing hard-codes a colour, radius, shadow or duration; everything
        reads a token, so a change lands everywhere at once.
     2. Every interactive element has a visible :focus-visible ring. The app is
        operable end to end without a mouse.
     3. Every colour token is defined twice — once here for the dark theme and
        once in the `[data-theme="light"]` block below. Rule 1 is what makes
        that possible: the two themes differ only in this file's first hundred
        lines, and no component knows which one it is being painted in.

   Two kinds of colour token, and the distinction matters when adding one:
     --x        a fill, a border, a dot. Stays saturated in both themes.
     --x-text   the same hue used as *text*, so it has to earn its contrast
                against the surface behind it — light in the dark theme, dark
                in the light one. These are not interchangeable: `var(--red)`
                as a label is unreadable on white.
   ========================================================================== */

:root {
  color-scheme: dark;

  /* --- surface ramp: --field is always strictly recessed from --panel ------ */
  --bg: #080b14;
  --bg-grad: radial-gradient(1200px 640px at 78% -10%, rgba(77, 124, 255, 0.10), transparent 62%);
  --panel: #101627;
  --panel-2: #161e33;
  --panel-3: #1c2540;
  --field: #0b1120;
  --sidebar-grad: linear-gradient(180deg, var(--panel) 0%, #0d1220 100%);
  --code-bg: #070b16;
  /* theme-color: dark #080b14, light #f4f6fb — kept in step by hand in
     index.html, login.html, maintenance.html and js/theme.js. */

  --border: #212b45;
  --border-strong: #2e3a5c;
  --scroll-thumb: var(--border-strong);
  --scroll-thumb-hi: #3b4a75;

  /* Four tiers, each clear of AA on every surface it is used on. */
  --text: #e9edfa;
  --text-dim: #b6c0dd;
  --muted: #98a5c6;
  --faint: #7a87b0;

  /* --- accents ----------------------------------------------------------- */
  /* --accent and --accent-hi are hues — links, rails, rings. --accent-fill is
     the only accent surface white text ever sits on. */
  --accent: #5b86ff;
  --accent-hi: #7ea0ff;
  --accent-press: #4a72e8;
  --accent-fill: #3a63e0;
  --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
  --accent-soft-2: color-mix(in srgb, var(--accent) 22%, transparent);
  --accent-line: color-mix(in srgb, var(--accent) 32%, transparent);
  --accent-text: #a8c0ff;
  --accent-text-hi: #d3deff;
  --on-accent: #fff;

  --green: #3ecf8e;
  --amber: #f5b544;
  --red: #f2635f;
  /* One meaning: this row is not real. */
  --purple: #a978ff;

  --green-text: #64e0ac;
  --amber-text: #f7c76a;
  --red-text: #ff8b87;
  --purple-text: #bd9bff;
  /* On a solid --red fill. Dark in both themes: the fill is light enough in
     the dark theme that white text would not clear 3:1. */
  --on-red: #2a0a0a;

  /* --- tint scale: three strengths per hue, written once ------------------- */
  /* wash = a fill, wash-2 = the stronger fill, line = a border. A theme
     override only has to restate the percentage. */
  --green-wash: color-mix(in srgb, var(--green) 14%, transparent);
  --green-wash-2: color-mix(in srgb, var(--green) 22%, transparent);
  --green-line: color-mix(in srgb, var(--green) 36%, transparent);
  --amber-wash: color-mix(in srgb, var(--amber) 14%, transparent);
  --amber-line: color-mix(in srgb, var(--amber) 36%, transparent);
  --red-wash: color-mix(in srgb, var(--red) 14%, transparent);
  --red-wash-2: color-mix(in srgb, var(--red) 22%, transparent);
  --red-line: color-mix(in srgb, var(--red) 36%, transparent);
  --purple-wash: color-mix(in srgb, var(--purple) 14%, transparent);
  --purple-line: color-mix(in srgb, var(--purple) 28%, transparent);

  /* --- washes: translucent layers that take the surface behind them -------- */
  --row-alt: rgba(255, 255, 255, .038);
  --row-hover: rgba(255, 255, 255, .055);
  --tint-1: rgba(255, 255, 255, .012);
  --hairline: rgba(255, 255, 255, .07);
  --overlay: rgba(4, 7, 14, .66);
  --edge-fade: rgba(0, 0, 0, .38);
  /* The frozen-column edge, cast sideways. Not part of the elevation ladder. */
  --pin-shadow: rgba(0, 0, 0, .32);
  --grabber: color-mix(in srgb, var(--muted) 55%, transparent);

  /* --- floating materials: one blur, one opacity, one scrim ---------------- */
  --float-bg: color-mix(in srgb, var(--panel) 88%, transparent);
  --float-blur: blur(12px);
  --scrim-blur: blur(3px);

  /* The browser draws the date picker's glyph in black; invert it for a dark
     field, leave it alone on a light one. */
  --date-icon: invert(.72);

  /* --- shape: marks, controls, small surfaces, panels, pills --------------- */
  --r-xs: 3px;
  --r-sm: 7px;
  --r: 10px;
  --r-lg: 14px;
  --r-pill: 999px;

  /* --- elevation: a rim, then three cast shadows -------------------------- */
  /* --sh-1 alone moves a pixel by 2/3/6 units over --bg, so it does not read in
     the dark theme. The rim is what actually makes a surface look raised. */
  --rim: inset 0 1px 0 rgba(255, 255, 255, .055);
  --sh-1: 0 1px 2px rgba(0, 0, 0, .28);
  --sh-2: 0 4px 14px -6px rgba(0, 0, 0, .5), 0 1px 2px rgba(0, 0, 0, .3);
  --sh-3: 0 18px 44px -18px rgba(0, 0, 0, .68), 0 2px 8px rgba(0, 0, 0, .34);
  --sh-4: 0 40px 90px -28px rgba(0, 0, 0, .8), 0 4px 14px rgba(0, 0, 0, .4);

  /* --- motion: three durations, two curves, paired by job ------------------ */
  --dur-1: 110ms;
  --dur-2: 190ms;
  --dur-3: 300ms;
  --ease: cubic-bezier(.32, .72, .28, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);

  /* --- space: seven steps ------------------------------------------------- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;

  /* --- metrics ----------------------------------------------------------- */
  --sidebar-w: 232px;
  --topbar-h: 56px;
  --tap: 40px;
  --ctl-h: 34px;
  --ctl-h-sm: 28px;
  --gutter: 26px;
  --measure: 68ch;

  /* Row geometry lives on tokens so the density switch can move all of it. */
  --row-pad-y: 10px;
  --row-pad-x: 11px;
  --row-lh: 1.35;

  /* --- typeface ----------------------------------------------------------- */
  --mono: ui-monospace, SFMono-Regular, "Cascadia Mono", "Cascadia Code", Consolas, monospace;
  /* system-ui first so Windows 11 resolves Segoe UI Variable, which is better
     fitted at 11-13px than the static family. */
  --sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  /* --- weight: three, because Windows 10's static Segoe UI ships three ----- */
  --w-regular: 400;
  --w-medium: 600;
  --w-bold: 700;

  /* --- type ramp ---------------------------------------------------------- */
  --fs-display: 28px;
  --lh-display: 1.15;
  --tr-display: -0.02em;

  --fs-title: 24px;
  --lh-title: 1.2;
  --tr-title: -0.015em;

  --fs-heading: 17px;
  --lh-heading: 1.3;

  --fs-section: 14px;
  --lh-section: 1.35;
  --fs-body: 14px;
  --lh-body: 1.5;
  --fs-dense: 13px;
  /* --lh-dense is --row-lh for blocks outside a table; the table itself reads
     --row-lh so the density switch can move it. */
  --lh-dense: 1.35;
  --fs-label: 12px;
  --fs-caption: 11.5px;
  --fs-caps: 11px;
  --tr-caps: .05em;
  --fs-micro: 10px;
}

/* ------------------------------ light theme ------------------------------ */
/*
 * Set by the theme toggle, which writes `data-theme` on <html> and remembers
 * the choice. Dark stays the default: an installation that never touches the
 * toggle looks exactly as it always did.
 *
 * Only colour is redefined here. Shape, motion and metrics are the same app in
 * either theme, and a token that appears in one block and not the other is a
 * bug waiting for whoever flips the switch.
 */
:root[data-theme="light"] {
  color-scheme: light;

  --bg: #f4f6fb;
  --bg-grad: radial-gradient(1200px 640px at 78% -10%, rgba(47, 95, 224, 0.07), transparent 62%);
  --panel: #ffffff;
  --panel-2: #f1f4fa;
  --panel-3: #e4e9f4;
  /* Recessed 1.055:1 from --panel, mirroring dark's 1.05:1. It was byte-
     identical to --panel, so every input and the progress track were the same
     colour as the card behind them. */
  --field: #f7f9fc;
  --sidebar-grad: linear-gradient(180deg, #ffffff 0%, #eef1f8 100%);
  --code-bg: #f6f8fc;

  --border: #d9e0ec;
  --border-strong: #bcc6d9;
  --scroll-thumb: var(--border-strong);
  --scroll-thumb-hi: #9aa7c0;

  --text: #131a2b;
  /* The taper collapsed at the bottom — --muted and --faint were 1.15:1 apart,
     so three declared tiers delivered two. Darken the middle, not the bottom. */
  --text-dim: #333f5d;
  --muted: #4a5771;
  --faint: #66748f;

  --accent: #2f5fe0;
  --accent-hi: #1f47c4;
  --accent-press: #17399f;
  --accent-fill: #2f5fe0;
  --accent-text: #2450cf;
  --accent-text-hi: #1a3fae;
  --on-accent: #fff;
  /* --accent-soft / --accent-soft-2 / --accent-line are not restated: the
     :root color-mix resolves against this block's --accent, so the two themes
     cannot drift apart the way a second literal rgba() did. */

  --green: #12a06a;
  --amber: #c9821a;
  --red: #dc4340;
  --purple: #7c4ddb;

  --green-text: #0a7148;
  --amber-text: #96610d;
  --red-text: #c02320;
  --purple-text: #6134c0;
  --on-red: #2a0a0a;

  /* Only the percentages that need more presence on white are restated. Amber
     at 10% over --panel #101627 is 1.198:1; over white it is 1.102:1. 18% over
     white measures 1.199:1, which is the dark theme's weight. */
  --green-wash: color-mix(in srgb, var(--green) 18%, transparent);
  --green-wash-2: color-mix(in srgb, var(--green) 26%, transparent);
  --green-line: color-mix(in srgb, var(--green) 44%, transparent);
  --amber-wash: color-mix(in srgb, var(--amber) 18%, transparent);
  --amber-line: color-mix(in srgb, var(--amber) 44%, transparent);

  /* Ink rather than white, and a shade cooler than pure black so the washes
     tint towards the app's blue instead of turning surfaces grey. */
  --row-alt: rgba(20, 30, 55, .038);
  --row-hover: rgba(20, 30, 55, .055);
  --tint-1: rgba(20, 30, 55, .022);
  --hairline: rgba(20, 30, 55, .12);
  --overlay: rgba(17, 24, 43, .38);
  --edge-fade: rgba(20, 30, 55, .16);
  --pin-shadow: rgba(20, 30, 55, .14);

  --date-icon: none;

  /* Shadows on a light ground read as dirt long before they read as depth, so
     every step is roughly a third of its dark-theme opacity. A rim would only
     fog the top edge here, so it is a deliberate no-op. */
  --rim: inset 0 1px 0 rgba(255, 255, 255, 0);
  --sh-1: 0 1px 2px rgba(20, 30, 55, .07);
  --sh-2: 0 4px 14px -6px rgba(20, 30, 55, .16), 0 1px 2px rgba(20, 30, 55, .06);
  --sh-3: 0 18px 44px -18px rgba(20, 30, 55, .24), 0 2px 8px rgba(20, 30, 55, .08);
  --sh-4: 0 40px 90px -28px rgba(20, 30, 55, .28), 0 4px 14px rgba(20, 30, 55, .10);
}

/* Density is a scope, not an override list. The class is on the table wrapper,
   so it only has to restate three numbers and the whole feature follows. */
.density-compact {
  --row-pad-y: 4px;
  --row-pad-x: 8px;
  --row-lh: 1.25;
}

/* ------------------------------- reset --------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scrollbar-color: var(--scroll-thumb) transparent;
}

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

body {
  background: var(--bg);
  background-image: var(--bg-grad);
  background-attachment: fixed;
  color: var(--text);
  font: var(--fs-body)/var(--lh-body) var(--sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-wrap: break-word;
}

body.no-scroll { overflow: hidden; }

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

/* Tracking follows size and nothing else: -0.02em at >=28px, -0.015em from
   17-24px, nothing below 17px. A blanket rule on h1-h4 tightened section
   headings below the size where tightening helps. */

::selection { background: var(--accent-line); color: var(--text); }

/*
 * One focus treatment for everything that can take focus, delivered as an
 * `outline` rather than a `box-shadow`.
 *
 * `:focus-visible` is a pseudo-class at specificity (0,1,0), so any later class
 * rule that declares `box-shadow` — .btn-primary, .skip-link, the corner badges
 * — used to take the property back and render no ring at all. `outline` is a
 * property nothing else is competing for, and it follows each element's own
 * border-radius, so pills and circles keep their shape.
 */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--accent-hi);
  outline-offset: 2px;
  position: relative;
  z-index: 3;
}

/* Scrollbars, so long lists do not look like a different app. */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb);
  border-radius: var(--r-pill);
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hi); background-clip: padding-box; }

.skip-link {
  position: fixed; top: 8px; left: 8px; z-index: 200;
  transform: translateY(-160%);
  padding: 10px 16px; border-radius: var(--r);
  background: var(--accent-fill); color: var(--on-accent); font-weight: var(--w-medium);
  box-shadow: var(--sh-3);
  transition: transform var(--dur-2) var(--ease-out);
}
.skip-link:focus-visible { transform: translateY(0); text-decoration: none; }

/* ------------------------------- shell --------------------------------- */

#app {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  grid-template-areas: "sidebar main";
  min-height: 100vh;
  min-height: 100dvh;
}

/* Both only exist below the drawer breakpoint; see the 860px block. */
.topbar { display: none; }
.nav-scrim { display: none; }

.sidebar {
  grid-area: sidebar;
  background: var(--sidebar-grad);
  border-right: 1px solid var(--border);
  padding: 16px 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.brand {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 10px 22px;
  font-weight: var(--w-bold); font-size: var(--fs-heading); letter-spacing: var(--tr-title);
}
.brand .dot {
  width: 11px; height: 11px; border-radius: 50%;
  background: linear-gradient(140deg, var(--accent-hi), var(--accent));
  box-shadow: 0 0 0 3px var(--accent-soft), 0 0 12px var(--accent-line);
  flex: none;
}

.nav-item {
  /* justify-content is spelled out because the base `button` rule centres its
     content, which would push the <button> sign-out row out of line with the
     <a> rows beside it. */
  display: flex; align-items: center; justify-content: flex-start; gap: 11px;
  min-height: var(--tap);
  padding: 9px 11px;
  border: 0; width: 100%;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--muted);
  font: inherit; font-weight: var(--w-regular); text-align: left;
  cursor: pointer; user-select: none;
  position: relative;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.nav-item:hover { background: var(--panel-2); color: var(--text); text-decoration: none; }
.nav-item:active { background: var(--panel-3); }
.nav-item.active { background: var(--accent-soft); color: var(--accent-text-hi); }
/* The active rail reads at a glance without relying on colour alone. */
/* Inset from the sidebar edge, so it reads as a rail rather than a stray mark
   pinned to the very edge of the window. */
.nav-item.active::before {
  content: ""; position: absolute; left: -6px; top: 50%;
  width: 3px; height: 18px; margin-top: -9px;
  border-radius: var(--r-pill);
  background: var(--accent-hi);
}
.nav-item .ico { display: inline-flex; width: 18px; height: 18px; opacity: .85; flex: none; }
.nav-item.active .ico { opacity: 1; }
.nav-spacer { flex: 1 1 auto; min-height: 12px; }

/* ---------------------------- network switcher --------------------------- */

/* Built by actionMenu, so it arrives carrying .menu-trigger.has-text — which
   sets its own padding, gap and letter-spacing for a table row. Put the nav
   item's geometry back, and give it a surface: this one names where you are
   rather than linking to a page, and should not read as a sixth destination. */
/* 8 + 1 and 10 + 1 restore the 9/11 content box the border-box border took
   away, so the glyph lands on the same 23px column as the six nav icons. */
.nav-item.nav-switch {
  gap: 11px;
  padding: 8px 10px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  color: var(--text-dim);
  margin-bottom: 6px;
}
.nav-item.nav-switch:hover { background: var(--panel-3); border-color: var(--border-strong); color: var(--text); }

/* A spacer, not a glyph: "⇄" is already the Integrations nav icon eight rows
   above, and one glyph should mean one thing. The switcher names the network in
   text inside its own bordered box and needs no mark of its own. */
.nav-item.nav-switch::before {
  content: "";
  width: 18px; flex: none;
}
.nav-item.nav-switch .menu-trigger-text {
  flex: 1 1 auto; min-width: 0;
  max-width: none;              /* the sidebar, not 15ch, is the constraint here */
  font-weight: var(--w-medium);
}
.nav-item.nav-switch .caret { flex: none; margin-left: 4px; }

.main {
  grid-area: main;
  padding: var(--sp-6) var(--gutter) var(--sp-7);
  min-width: 0;
}

/* One rule for the rhythm between top-level blocks on a page. The second
   selector reaches the views that mount their blocks inside a bare host div. */
.main > * + *, .main > div > * + * { margin-top: var(--sp-4); }

/* ---------------------------- corner badges ----------------------------- */

/* The build badge and the update notification share this row. It owns the
   fixed positioning so the badges themselves stay ordinary inline elements and
   can also be dropped into the app bar on a phone. */
.corner-badges {
  position: fixed;
  top: 12px; right: var(--gutter);
  z-index: 40;
  display: flex; align-items: center; gap: var(--sp-2);
}

/* Opaque, which is what the phone block already does. Translucency here bought
   nothing — these badges mostly float over empty page background — and it made
   the two halves of a deliberately matched cluster disagree on hover. */
.version-badge {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: var(--ctl-h-sm);
  padding: 4px 11px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: var(--panel-2);
  color: var(--muted);
  font-size: var(--fs-caption); font-family: var(--mono);
  cursor: pointer; user-select: none;
  box-shadow: var(--rim), var(--sh-1);
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.version-badge:hover { color: var(--text); border-color: var(--border-strong); transform: translateY(-1px); box-shadow: var(--rim), var(--sh-2); }
.version-badge:active { transform: translateY(0); }
.version-badge .env { color: var(--accent-hi); font-weight: var(--w-medium); }
.version-badge .hash { opacity: .7; }
.version-badge .dot-live {
  width: 6px; height: 6px; border-radius: 50%; background: var(--green);
  box-shadow: 0 0 0 3px var(--green-wash);
}

/* ------------------------------ theme toggle ----------------------------- */

/*
 * Rides in the corner row beside the build badge, and moves into the app bar on
 * a phone with it. Sized to match the badge so the two read as one cluster
 * rather than a control that wandered in.
 */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctl-h-sm); height: var(--ctl-h-sm);
  min-height: 0; padding: 0; flex: none;
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: var(--panel-2);
  color: var(--muted);
  cursor: pointer;
  box-shadow: var(--rim), var(--sh-1);
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
/* No background of its own: `button:hover` now applies to both badges
   identically, which is what stopped them diverging. */
.theme-toggle:hover {
  color: var(--text); border-color: var(--border-strong); transform: translateY(-1px);
  box-shadow: var(--rim), var(--sh-2);
}
.theme-toggle:active { transform: translateY(0); }
.theme-toggle svg { display: block; }

/* Only one of the two glyphs is ever drawn: the icon shows the theme you would
   get by pressing it, not the one you are already in. */
.theme-toggle .ico-sun { display: none; }
:root[data-theme="light"] .theme-toggle .ico-sun { display: block; }
:root[data-theme="light"] .theme-toggle .ico-moon { display: none; }

@media (max-width: 860px) {
  .theme-toggle { width: 32px; height: 32px; }
}

/* --------------------------- update available --------------------------- */

/* Sits to the left of the build badge and is the only accent-coloured thing in
   the corner, so it reads as "something is waiting for you" without being an
   interruption. Nothing here blocks the page — the operator picks the moment. */
.update-badge {
  order: -1;
  display: inline-flex; align-items: center; gap: 7px;
  min-height: var(--ctl-h-sm);
  padding: 4px 12px;
  border-radius: var(--r-pill);
  border: 1px solid var(--accent-line);
  background: var(--accent-soft);
  color: var(--accent-hi);
  font-size: var(--fs-caption); font-weight: var(--w-medium);
  cursor: pointer; user-select: none;
  box-shadow: var(--rim), var(--sh-1);
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.update-badge:hover { background: var(--accent-soft-2); transform: translateY(-1px); box-shadow: var(--rim), var(--sh-2); }
.update-badge:active { transform: translateY(0); }

.update-badge-dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent-hi);
  animation: update-pulse 2.4s var(--ease) infinite;
}
@keyframes update-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-hi) 50%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent-hi) 0%, transparent); }
}
/* A pulsing dot is an accessibility problem for anyone who asked the system to
   stop moving things. The badge is still visible; it just holds still. */
@media (prefers-reduced-motion: reduce) {
  .update-badge-dot { animation: none; }
}

.update-versions {
  display: flex; align-items: center; gap: 18px;
  padding: 14px var(--sp-4); margin-bottom: 18px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
}
.update-arrow { color: var(--muted); font-size: 18px; }
.update-h4 { margin: 0 0 10px; font-size: var(--fs-section); line-height: var(--lh-dense); font-weight: var(--w-medium); }

.change-list { margin: 0; padding: 0; list-style: none; max-height: 220px; overflow-y: auto; }
.change-list li {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.change-list li:last-child { border-bottom: 0; }
.change-subject { min-width: 0; overflow-wrap: anywhere; }

/* The consequence of pressing the button, stated before the button is pressed. */
/* The stripe is an inset shadow rather than a 3px border: a border of a
   different width miters across the corner arc and fans the stripe out through
   the curve, where an inset shadow follows the radius exactly. */
.update-warn {
  margin-top: 18px; padding: var(--sp-3) 14px;
  box-shadow: inset 3px 0 0 var(--amber);
  border-radius: var(--r-sm);
  background: var(--amber-wash);
  font-size: var(--fs-dense); color: var(--text-dim);
}
.update-warn strong { color: var(--text); }

.update-progress-panel { padding: 4px 0; }

.progress-bar {
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--field);
  border: 1px solid var(--border);
  overflow: hidden;
}
.progress-fill {
  height: 100%; width: 0%;
  border-radius: var(--r-pill);
  background: var(--accent-fill);
  transition: width .9s var(--ease);
}
/* Before the first reading there is no number to show, so the bar says
   "something is happening" rather than claiming a value it does not have. */
.progress-bar.unknown .progress-fill {
  width: 34% !important;
  opacity: .6;
  animation: progress-sweep 1.5s ease-in-out infinite;
}
@keyframes progress-sweep { 0% { margin-left: -34%; } 100% { margin-left: 100%; } }
.progress-bar.failed .progress-fill { background: var(--amber); animation: none; }

.progress-meta {
  display: flex; justify-content: space-between;
  margin-top: 9px;
  font-size: var(--fs-label); color: var(--faint);
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .progress-fill { transition: none; }
  .progress-bar.unknown .progress-fill { animation: none; }
}

/* ------------------------------ fraud score ----------------------------- */

.fraud-pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 3px 10px 3px 4px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: var(--panel-2);
  color: var(--text-dim);
  font-size: var(--fs-caption); font-weight: var(--w-medium);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease);
}
.fraud-pill:hover { border-color: var(--border-strong); transform: translateY(-1px); box-shadow: var(--sh-2); }
/* The most-pressed pill in the table had no press beat at all: .fraud-pill:hover
   is (0,2,0) and out-ranks `button:active` at (0,1,1). */
.fraud-pill:active { transform: translateY(0); }
.fraud-pill.static { cursor: default; }
.fraud-pill.static:hover { transform: none; box-shadow: none; }

/* A paid, one-shot check arriving in the cell the operator pressed. */
.fraud-pill.is-new { animation: pill-in var(--dur-2) var(--ease-out) both; }
@keyframes pill-in {
  from { opacity: 0; transform: scale(.92); }
  to { opacity: 1; transform: none; }
}

.fraud-pill-score {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 26px; height: 20px; padding: 0 5px;
  border-radius: var(--r-pill);
  background: var(--panel-3);
  font-family: var(--mono); font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.fraud-pill-label { padding-right: 2px; }


/* Colour carries the same information as the label, never instead of it —
   the band name is always present for anyone who cannot separate the hues. */
.fraud-pill.clean      { border-color: var(--green-line); color: var(--green-text); }
.fraud-pill.clean      .fraud-pill-score { background: var(--green-wash); }
.fraud-pill.suspicious { border-color: var(--amber-line); color: var(--amber-text); }
.fraud-pill.suspicious .fraud-pill-score { background: var(--amber-wash); }
.fraud-pill.high       { border-color: var(--red-line); color: var(--red-text); }
.fraud-pill.high       .fraud-pill-score { background: var(--red-wash); }
.fraud-pill.fraud      { border-color: var(--red); color: var(--text); background: var(--red-wash-2); }
.fraud-pill.fraud      .fraud-pill-score { background: var(--red); color: var(--on-red); }

/* The row itself, so a fraud lead is obvious while scanning even when the
   Fraud column is hidden. A left edge plus a wash, not a full red fill: the
   row must stay readable.

   Set through --row-bg rather than on the cells directly, because a frozen
   column has to paint the same colour as the row it belongs to — see the
   row-shading block further down. */
tbody tr.row-fraud { --row-bg: var(--red-wash); }
tbody tr.row-fraud:hover { --row-bg: var(--red-wash-2); }
tbody tr.row-fraud > td:first-child { box-shadow: inset 3px 0 0 var(--red); }

.fraud-headline {
  display: flex; align-items: center; gap: 18px;
  padding: var(--sp-4); margin-bottom: 18px;
  border-radius: var(--r);
  border: 1px solid var(--border);
  background: var(--panel-2);
}
/* A 0-100 score, not a hero number: on the display step, not two above it. */
.fraud-headline-score {
  font-size: var(--fs-display); font-weight: var(--w-medium); letter-spacing: var(--tr-display);
  font-variant-numeric: tabular-nums; line-height: 1;
  min-width: 62px; text-align: center;
}
.fraud-headline.clean      { border-color: var(--green-line); }
.fraud-headline.clean      .fraud-headline-score { color: var(--green-text); }
.fraud-headline.suspicious { border-color: var(--amber-line); }
.fraud-headline.suspicious .fraud-headline-score { color: var(--amber-text); }
.fraud-headline.high       { border-color: var(--red-line); }
.fraud-headline.high       .fraud-headline-score { color: var(--red-text); }
.fraud-headline.fraud      { border-color: var(--red); background: var(--red-wash); }
.fraud-headline.fraud      .fraud-headline-score { color: var(--red-text); }

.fraud-escalated {
  padding: var(--sp-3) 14px; margin-bottom: 4px;
  box-shadow: inset 3px 0 0 var(--amber);
  border-radius: var(--r-sm);
  background: var(--amber-wash);
  font-size: var(--fs-dense);
}
.fraud-escalated ul { margin: 6px 0 0; padding-left: 18px; }

.fraud-section { padding: 14px 0; border-top: 1px solid var(--border); }
.fraud-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.fraud-section-head h4 { margin: 0; font-size: var(--fs-section); line-height: var(--lh-dense); font-weight: var(--w-medium); }

.fraud-flags { list-style: none; margin: 0 0 12px; padding: 0; display: grid; gap: 6px; }
.fraud-flags li { display: flex; align-items: baseline; gap: 8px; font-size: var(--fs-dense); color: var(--text-dim); }
.fraud-flag-dot { width: 6px; height: 6px; border-radius: 50%; flex: none; background: var(--amber); }
.fraud-flags li.critical { color: var(--text); }
.fraud-flags li.critical .fraud-flag-dot { background: var(--red); }

.fraud-detail td { padding: 5px 10px 5px 0; vertical-align: top; }
.fraud-detail tr + tr td { border-top: 1px solid var(--border); }

/* ------------------------------ page head ------------------------------- */

.page-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap;
}
.page-head h1 {
  margin: 0; font-size: var(--fs-title); line-height: var(--lh-title);
  letter-spacing: var(--tr-title); font-weight: var(--w-medium);
}
.page-head .sub {
  margin: 5px 0 0; color: var(--muted);
  font-size: var(--fs-dense); line-height: var(--lh-body); max-width: var(--measure);
}
.head-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; }
/* A 25px pill in a row of 34px buttons breaks the toolbar's top and bottom
   edge; the 28px step is the one the rest of the system already has. */
.head-actions > .pill { min-height: var(--ctl-h-sm); padding-inline: var(--sp-3); }

/* ------------------------------ surfaces -------------------------------- */

.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  box-shadow: var(--rim), var(--sh-1);
}
.card h3 { margin: 0 0 var(--sp-3); font-size: var(--fs-section); line-height: var(--lh-section); font-weight: var(--w-medium); }

/*
 * A card whose only child is its own scroller. `overflow: hidden` clips at the
 * padding box, which with zero padding and a 1px border is exactly the
 * concentric inner radius — so the opaque sticky header stops terminating in a
 * hard right angle inside the card's arc. Scoped to `.flush` because clipping
 * every card would cut focus rings off across the app.
 */
.card.flush { padding: 0; overflow: hidden; }

/* Prose in a card gets a measure. The child combinator matters: as a descendant
   selector this would also clamp the fraud flag list and the log entries. */
.card > p, .card > ul, .empty > div { max-width: var(--measure); }
.empty > div { margin-inline: auto; }

/* Row of secondary actions at the foot of a card. */
.card-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; }
@media (max-width: 560px) {
  .card-actions > button, .card-actions > .btn { flex: 1 1 calc(50% - 4px); justify-content: center; }
}

.grid { display: grid; gap: var(--sp-3); }
.grid > label.f { margin-bottom: 0; }
/* Cards of genuinely different length (a 2-row list beside a 12-row one) look
   broken when stretched to match — hundreds of pixels of empty panel. */
.grid.align-start { align-items: start; }
.grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* Same material as .card and as its own skeleton: a gradient standing in for
   elevation was the one place in the file where two surfaces at one height
   were made of different things. */
.stat {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--sp-3) var(--sp-4);
  box-shadow: var(--rim), var(--sh-1);
}
/* No :hover lift — it is a plain div with no handler, and a false affordance is
   not improved by making it more convincing. */
.stat .label { color: var(--muted); font-size: var(--fs-caps); font-weight: var(--w-medium); }
.stat .value {
  font-size: var(--fs-display); font-weight: var(--w-medium); margin-top: 7px;
  letter-spacing: var(--tr-display); font-variant-numeric: tabular-nums;
  line-height: var(--lh-display);
}
.stat .foot { color: var(--faint); font-size: var(--fs-caption); margin-top: 5px; }
/* A smaller tile, for a dialog. A modifier, not an inline style. */
.stat.stat-sm { padding: var(--sp-3); }
.stat.stat-sm .value { font-size: 22px; }
.stat .value.green { color: var(--green-text); }
.stat .value.amber { color: var(--amber-text); }
.stat .value.red { color: var(--red-text); }

/* ------------------------------- buttons -------------------------------- */

/*
 * `:where()` contributes zero specificity, so `.bare` can opt a control out
 * without the base rule out-ranking .btn-primary and friends. `button:not(.bare)`
 * would be (0,1,1) and would strip the fill off every button variant.
 */
.btn, button:where(:not(.bare)) {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font: inherit; font-weight: var(--w-medium);
  min-height: var(--ctl-h);
  padding: 7px 13px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--panel-2);
  color: var(--text);
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.btn:hover, button:where(:not(.bare)):hover {
  background: var(--panel-3); border-color: var(--border-strong);
  text-decoration: none; color: var(--text);
}
.btn:active, button:where(:not(.bare)):active { transform: translateY(1px); }
.btn[aria-disabled="true"], button:where(:not(.bare)):disabled {
  opacity: .5; cursor: not-allowed; transform: none; background: var(--panel-2);
}

/*
 * Chrome-less controls. Never `all: unset` — that resets box-shadow at class
 * specificity and would take the focus ring off every bare button, which is the
 * cascade bug the outline ring exists to fix.
 */
.bare {
  min-height: 0; padding: 0;
  border: 0; border-radius: 0;
  background: none; color: inherit; font: inherit;
  display: inline-flex; align-items: center;
  cursor: pointer;
}

/* Flat, because white on the dark theme's gradient measured 2.51:1 at its top
   stop. --accent-fill is 5.20:1 resting and --accent-press 4.32:1 on hover, and
   the fill still holds 3.47:1 against --panel so the button reads as an object. */
.btn-primary {
  background: var(--accent-fill);
  border-color: var(--accent-press);
  color: var(--on-accent);
  font-weight: var(--w-medium);
  --rim: inset 0 1px 0 rgba(255, 255, 255, .16);
  box-shadow: var(--rim), var(--sh-1);
}
.btn-primary:hover { background: var(--accent-press); border-color: var(--accent); color: var(--on-accent); }
.btn-primary:disabled { background: var(--accent-fill); }

.btn-danger { color: var(--red-text); }
.btn-danger:hover {
  background: var(--red-wash);
  border-color: var(--red-line);
  color: var(--red-text);
}

.btn-sm { min-height: var(--ctl-h-sm); padding: 4px 10px; font-size: 12.5px; }
.btn-ghost { background: transparent; border-color: transparent; }
.btn-ghost:hover { background: var(--panel-2); border-color: var(--border); }
/* A third tier that keeps a resting edge, so it does not read as static text. */
.btn-quiet { background: transparent; border-color: var(--border); color: var(--text-dim); }
.btn-quiet:hover { background: var(--panel-2); color: var(--text); }

/*
 * The square icon button, unbound from --tap so it can serve the dialog close
 * and the toast dismiss as well as the phone's nav toggle.
 */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctl-h); height: var(--ctl-h);
  min-height: 0; padding: 0; flex: none;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  background: transparent; color: var(--text-dim);
  cursor: pointer; line-height: 1;
}
.icon-btn.sm { width: 26px; height: 26px; }
.icon-btn.lg { width: var(--tap); height: var(--tap); }
.icon-btn:hover { background: var(--panel-2); border-color: var(--border); color: var(--text); }

/*
 * The busy state. The label stays in the box holding the width, so a button
 * cannot resize itself mid-press, and `disabled` is never set — disabling a
 * focused button drops the focus and strands a keyboard user mid-action.
 * var(--text), not currentColor: currentColor is transparent on this element.
 */
/* .switch is excluded: it paints its new state optimistically on click, which
   is stronger feedback than a spinner, and it has its own busy rule below. */
button:where(:not(.switch))[aria-busy="true"], .btn[aria-busy="true"] { cursor: progress; color: transparent; position: relative; }
button:where(:not(.switch))[aria-busy="true"]::after, .btn[aria-busy="true"]::after {
  content: ''; position: absolute; inset: 0; margin: auto;
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--text) 25%, transparent);
  border-top-color: var(--text);
  animation: spin .7s linear infinite;
}
.btn-primary[aria-busy="true"]::after {
  border-color: color-mix(in srgb, var(--on-accent) 25%, transparent);
  border-top-color: var(--on-accent);
}
@keyframes spin { to { transform: rotate(360deg); } }
/* The global reduced-motion block flattens animations to 1ms, which would
   freeze the spinner mid-arc. Say it in words instead. */
@media (prefers-reduced-motion: reduce) {
  button:where(:not(.switch))[aria-busy="true"], .btn[aria-busy="true"] { color: inherit; opacity: .6; }
  button:where(:not(.switch))[aria-busy="true"]::after, .btn[aria-busy="true"]::after { display: none; }
}

/* ------------------------------- inputs --------------------------------- */

input, select, textarea {
  font: inherit; width: 100%;
  min-height: var(--ctl-h);
  padding: 7px 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--field);
  color: var(--text);
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
input::placeholder, textarea::placeholder { color: var(--faint); }
input:hover, select:hover, textarea:hover { border-color: var(--border-strong); }
input:focus, select:focus, textarea:focus,
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
input[type="checkbox"], input[type="radio"] {
  width: 17px; height: 17px; min-height: 0;
  accent-color: var(--accent);
  cursor: pointer;
}
input[type="color"] { padding: 3px; height: var(--ctl-h); cursor: pointer; }
/* The native date/time glyphs are black by default; --date-icon inverts them
   for a dark field and leaves them alone on a light one. */
input[type="date"]::-webkit-calendar-picker-indicator { filter: var(--date-icon); cursor: pointer; }
input[type="date"] { min-width: 0; }

/* Monospace says "compare this character by character", which is a claim about
   a JSON body and not about a sentence somebody typed. */
textarea { min-height: 92px; font-size: var(--fs-dense); line-height: var(--lh-body); resize: vertical; }
textarea.code { font-family: var(--mono); font-size: 12.5px; line-height: 1.55; tab-size: 2; }

select {
  appearance: none;
  padding-right: 30px;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
                    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 15px) calc(50% + 1px), calc(100% - 10px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

label.f { display: block; margin-bottom: var(--sp-3); }
label.f > span { display: block; font-size: var(--fs-label); font-weight: var(--w-medium); color: var(--text-dim); margin-bottom: 6px; }
label.f > small { display: block; color: var(--faint); margin-top: 5px; font-size: var(--fs-caption); }
label.f > span .req { color: var(--red-text); margin-left: 3px; }

/* Grids own the spacing inside themselves; this puts the rhythm back between
   them, which the label margin was silently providing. */
.modal-body > .grid + .grid,
.modal-body > .grid + label.f,
.modal-body > label.f + .grid { margin-top: var(--sp-3); }

/*
 * The error state. Declared after input:focus-visible, which sets its own
 * box-shadow at equal specificity and would otherwise win on source order.
 */
label.f.is-invalid > span { color: var(--red-text); }
label.f.is-invalid input,
label.f.is-invalid select,
label.f.is-invalid textarea {
  border-color: var(--red);
  box-shadow: 0 0 0 3px var(--red-wash);
}
.f-err { display: block; color: var(--red-text); margin-top: 5px; font-size: var(--fs-caption); line-height: 1.5; }

.inline { display: flex; align-items: center; gap: var(--sp-2); }
.inline input[type=checkbox] { width: 17px; }
label.inline { cursor: pointer; min-height: var(--tap); }

/* ------------------------------- tables --------------------------------- */

.table-wrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  /* Fading edges signal "there is more sideways" without an extra element. */
  background:
    linear-gradient(to right, var(--panel) 30%, transparent) left / 34px 100% no-repeat local,
    linear-gradient(to left, var(--panel) 30%, transparent) right / 34px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, var(--edge-fade), transparent) left / 14px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, var(--edge-fade), transparent) right / 14px 100% no-repeat scroll;
}

/* A decided leading, not body prose inherited: 13px x 1.55 gave a 20.15px line
   box in a table of one-line cells, and it was the one part of row height the
   density control could not reach. Tabular figures are a property of the table
   rather than of whichever component someone remembered. */
table {
  width: 100%; border-collapse: separate; border-spacing: 0;
  font-size: var(--fs-dense); line-height: var(--row-lh);
  font-variant-numeric: tabular-nums;
}
th, td {
  text-align: left; padding: var(--row-pad-y) var(--row-pad-x);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
/**
 * `position: sticky` on a header only works against a scrollport that actually
 * scrolls in that axis. `.table-wrap` is `overflow-x: auto`, which makes it a
 * scroll container in both axes, so a page-level scroll never triggers it —
 * the header just scrolled away. `.table-wrap.tall` gives the container its own
 * vertical scroll, which is what the sticky header needs, and keeps the filters
 * and pager on screen while you move through rows.
 */
.table-wrap.tall {
  max-height: min(68dvh, 760px);
  overflow-y: auto;
}

/* Sentence case: uppercase strips the ascenders and descenders that let an
   operator find "Registration" among fourteen headers at a glance. The header
   row loses nothing — the panel-2 fill and the strong rule are what actually
   say "this is not data". */
thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--panel-2);
  color: var(--muted); font-weight: var(--w-medium); font-size: var(--fs-caption);
  white-space: nowrap;
  border-bottom: 1px solid var(--border-strong);
}
/* ---------------------------- row shading -------------------------------- */
/*
 * Every row states its own background in one variable, and the cells read it.
 *
 * The indirection earns its keep as soon as a column is frozen: a sticky cell
 * has the rest of the table sliding underneath it, so it cannot be transparent,
 * and hard-coding a panel colour into it would leave the frozen cells the same
 * shade while the row behind them striped, highlighted on hover, or turned red
 * for fraud. One variable, set on the row, keeps every cell in step.
 */
/* The transition lives where the paint happens. `tr` never paints a background,
   so a transition declared on it was inert and both hover and selection
   snapped. `background-color`, not the shorthand — the shorthand resets
   background-image, which is how a frozen cell carries the same tint. */
tbody tr { --row-bg: transparent; }
tbody tr:nth-child(even) { --row-bg: var(--row-alt); }
tbody tr:hover { --row-bg: var(--row-hover); }
tbody tr.is-selected { --row-bg: var(--accent-soft-2); }
tbody tr.is-selected:hover { --row-bg: color-mix(in srgb, var(--accent) 28%, transparent); }
tbody tr > td {
  background-color: var(--row-bg);
  transition: background-color var(--dur-1) var(--ease);
}
tbody tr:last-child td { border-bottom: 0; }

/* A selected row says so at its left edge as well, so a scattered selection is
   countable down the page rather than only in the pager's total. */
tbody tr.is-selected > td:first-child {
  box-shadow: inset 3px 0 0 var(--accent);
  transition: background-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}

/* Sort controls are real buttons so the keyboard reaches them. Carrying `.bare`
   they start with no chrome, so this is geometry only. */
.th-sort {
  align-items: center; justify-content: flex-start; gap: 3px;
  padding: 2px 4px; margin: -2px -4px;
  text-transform: inherit; letter-spacing: inherit;
}
/* Both states, so Space does not move the label 1px inside a sticky header. */
.th-sort:hover, .th-sort:active { color: var(--text); transform: none; }
/*
 * The caret is reserved rather than injected, and `flex: none` on it with
 * `min-width: 0` on the label is what guarantees the label ellipsises and the
 * sort indicator is never the thing clipped.
 */
.th-sort .th-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.th-sort .caret {
  flex: none; display: inline-block; font-size: 8px; opacity: 0;
  transition: opacity var(--dur-1) var(--ease), transform var(--dur-2) var(--ease);
}
th:hover .th-sort .caret, .th-sort:focus-visible .caret { opacity: .45; }
th[aria-sort] .th-sort .caret { opacity: .9; }
th[aria-sort="ascending"] .th-sort .caret { transform: rotate(180deg); }
th[aria-sort] .th-sort { color: var(--text); }

td.num, th.num { text-align: right; }
/* Otherwise the sort button stays pinned left inside a right-aligned cell. */
th.num .th-sort { justify-content: flex-end; }

/* --------------------------- the leads table ----------------------------- */
/*
 * Fixed layout, widths from the <colgroup>.
 *
 * With the default algorithm the browser sizes each column from its contents,
 * so one unusually long brand name on page two moves every column on the
 * screen — the table has to be re-read from scratch on each page. Fixed layout
 * means a column is where it was last time, and it is also what makes a frozen
 * column's offset calculable before anything is measured.
 */
.table-fixed { table-layout: fixed; }

/*
 * The tick box is 17px in a 38px column; the label is what makes the whole
 * cell the target. Padding on the label rather than `height: 100%`, which does
 * not resolve reliably inside a <td>.
 */
th.cell-select, td.cell-select { padding: 0; }
.cell-tick {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 26px; padding: 6px 0;
  cursor: pointer;
}

/* The row-action cell holds buttons, not text, so it needs no ellipsis — and
   `overflow: hidden` there would clip the focus ring off every one of them. */
.leads-table td.cell-actions { overflow: visible; }
/* Chrome's scroll-into-view does not account for a sticky header. */
.leads-table tbody td { scroll-margin-top: 44px; }
/* The row that lights up on hover is now the thing that acts. */
.leads-table tbody tr { cursor: pointer; }

/*
 * One line per cell, with an ellipsis where it does not fit. The full value is
 * on the cell's title attribute, so nothing is actually lost — and a wrapped
 * cell would push its row to two lines and destroy the horizontal rhythm that
 * makes a long table scannable at all.
 */
.leads-table td, .leads-table th { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The lead's name, with its id underneath: one column, one glance, and the
   thing every other cell on the row is read against. */
.cell-lead { white-space: normal; }
.cell-lead .lead-name {
  display: block; color: var(--text); font-weight: var(--w-medium);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cell-lead .lead-meta {
  display: flex; align-items: center; gap: 6px;
  margin-top: 1px; font-size: var(--fs-caps); color: var(--muted);
}
.cell-lead .lead-meta a { color: var(--muted); text-decoration: none; }
.cell-lead .lead-meta a:hover { color: var(--accent); text-decoration: underline; }
/* A state with a real consequence — this row stays out of the figures and must
   not be worked — should not be announced in the smallest type in the product.
   10px/1.4 is a 14px box, so the meta row does not grow and nothing reflows. */
.cell-lead .lead-meta .pill { padding: 0 5px; font-size: var(--fs-micro); line-height: 1.4; }

/* Supporting detail — addresses, devices, timestamps — reads quieter than the
   facts an operator is scanning for. */
.leads-table td.muted-cell { color: var(--muted); }

/* ----------------------------- frozen columns ---------------------------- */
/*
 * Columns the operator has pinned stay against the left edge while the rest of
 * the table scrolls under them. Without it, scrolling right to reach Brand
 * status leaves a row of statuses belonging to nobody in particular.
 *
 * `left` is written inline by the view, because it is the sum of the widths
 * before it and only the view knows which columns are frozen today.
 */
.leads-table th.is-pinned, .leads-table td.is-pinned { position: sticky; z-index: 1; }
/* Above the body's frozen cells, and above the ordinary sticky header. */
.leads-table thead th.is-pinned { z-index: 4; }

/*
 * A frozen cell must be opaque — the rest of the table is sliding underneath
 * it — but it still has to show the row's own shade. So: the panel colour
 * underneath as a solid, and the row's tint painted over it as an image.
 * Setting --row-bg alone would leave the frozen cells see-through, since that
 * variable is a translucent tint and, for an ordinary cell, the card behind it
 * supplies the solid.
 */
.leads-table td.is-pinned {
  background-color: var(--panel);
  background-image: linear-gradient(var(--row-bg), var(--row-bg));
  transition: background-image var(--dur-1) var(--ease);
}
.leads-table thead th.is-pinned { background: var(--panel-2); }

/*
 * The boundary, as the cell's own outer box-shadow.
 *
 * The previous ::after was `translateX(100%)` inside a cell with
 * `overflow: hidden`, so it was parked entirely in clipped territory and never
 * rendered once — while the `border-right` beside it drew unconditionally,
 * which is the exact inversion of the intent. An outer shadow is not clipped by
 * the element's own overflow, and the pinned cell's z-index already lifts it
 * above the scrolling cells.
 */
.leads-table .is-pin-edge { transition: box-shadow var(--dur-1) var(--ease); }
.leads-wrap.is-scrolled .leads-table .is-pin-edge { box-shadow: 8px 0 10px -8px var(--pin-shadow); }
/* The two compound cases have to re-declare the inset edge they would drop. */
.leads-wrap.is-scrolled tbody tr.is-selected > td:first-child.is-pin-edge {
  box-shadow: inset 3px 0 0 var(--accent), 8px 0 10px -8px var(--pin-shadow);
}
.leads-wrap.is-scrolled tbody tr.row-fraud > td:first-child.is-pin-edge {
  box-shadow: inset 3px 0 0 var(--red), 8px 0 10px -8px var(--pin-shadow);
}

/*
 * The leads table's own scroll cue. The shared four-layer version paints on
 * .table-wrap's background, i.e. under every cell — and the first column here
 * is always frozen and always opaque, so the left pair was covered in every
 * row. The pin-edge shadow is that side's cue; only the right side needs a fade.
 */
.leads-wrap {
  background:
    linear-gradient(to left, var(--panel) 30%, transparent) right / 34px 100% no-repeat local,
    radial-gradient(farthest-side at 100% 50%, var(--edge-fade), transparent) right / 14px 100% no-repeat scroll;
  transition: opacity var(--dur-1) var(--ease);
}
/*
 * Refining a list does not destroy it. Opacity only — no pointer-events: none,
 * because the operator is still reading and scrolling those rows, and a click
 * on a stale row is harmless: selection is keyed by lead id.
 */
.leads-wrap.is-stale { opacity: .55; }

/*
 * The pager is the card's last row rather than a sibling below it, so Prev and
 * Next sit permanently at the foot of the rows, mirroring the sticky header at
 * the top. Not `position: sticky` — the pager is the last child of its parent
 * in every version of this layout, so sticky has exactly zero travel.
 */
.leads-surface { display: flex; flex-direction: column; max-height: min(72dvh, 816px); }

/*
 * The leads page owns the viewport.
 *
 * `height`, not `min-height`: a flex container only has free space to hand out
 * when its own height is definite. As a grid item `.main` stretches to its
 * content, so `flex: 1` on the card distributed nothing and the table simply
 * grew to 3500px and pushed the page into a scroll.
 *
 * With a definite 100dvh the leftover height goes to the card, the sticky
 * header and the pager stay put, and the rows scroll inside — which is the
 * layout the pager-in-the-card work was for.
 */
@media (min-width: 861px) {
  .main.view-fill {
    display: flex;
    flex-direction: column;
    height: 100dvh;
    /* --sp-7 is trailing space under the last card of a page that scrolls. Here
       nothing follows the table, so it is just height the rows could have had. */
    padding-bottom: var(--sp-5);
    /* Only ever engages if the head and an open filter bar leave less than the
       card's floor. Better a scrollbar in that corner case than clipped rows. */
    overflow: auto;
  }
  .main.view-fill > * { flex: none; }
  .main.view-fill > .leads-surface {
    flex: 1 1 auto;
    max-height: none;
    /* A floor, so a tall filter bar squeezes the page rather than the rows. */
    min-height: 320px;
  }
}
.leads-scroll { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.leads-scroll > .table-wrap.tall { flex: 1 1 auto; min-height: 0; max-height: none; }
.leads-pager {
  flex: none;
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 10px 14px;
  background: var(--panel-2);
  border-top: 1px solid var(--border);
  border-radius: 0 0 calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px);
}

/*
 * The right-hand freeze. `actions` is `locked` and sanitisePinned refuses to
 * pin it, so the column the view itself calls indispensable was off-screen at
 * rest on a 1440 or 1512 laptop with no way for the operator to rescue it.
 */
.leads-table th.is-pinned-right, .leads-table td.is-pinned-right { position: sticky; right: 0; z-index: 1; }
.leads-table thead th.is-pinned-right { z-index: 4; background: var(--panel-2); }
.leads-table td.is-pinned-right {
  background-color: var(--panel);
  background-image: linear-gradient(var(--row-bg), var(--row-bg));
  transition: background-image var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.leads-wrap.is-scrolled-end .leads-table .is-pinned-right { box-shadow: -8px 0 10px -8px var(--pin-shadow); }
@media (max-width: 560px) {
  .table-wrap.as-cards th.is-pinned-right,
  .table-wrap.as-cards td.is-pinned-right { position: static; box-shadow: none; }
}

/*
 * The row focus indicator has to be an outline. An inset box-shadow on a <tr>
 * paints in the row's own background layer, which every cell background paints
 * over and which the opaque frozen cells hide entirely. All three overrides
 * undo the global ring: z-index in particular, because `z-index: 3` would lift
 * a focused row above the sticky header at 2.
 */
.leads-table tbody tr:focus-visible {
  outline: 2px solid var(--accent-hi);
  outline-offset: -2px;
  box-shadow: none; border-radius: 0; z-index: auto;
}

/* The queue says it has changed rather than reordering under the operator. */
.new-leads-bar {
  flex: none;
  display: flex; justify-content: center;
  padding: 8px;
  border-bottom: 1px solid var(--border);
  background: var(--accent-soft);
}

/*
 * Two grouping devices for one job, and the louder one was the boxes-and-borders
 * device. The stripe survives horizontal scrolling for free through --row-bg;
 * the hairline does not. Kept on every other table in the app.
 */
.leads-table tbody td { border-bottom: 0; }

.empty { padding: var(--sp-6) var(--sp-5); text-align: center; color: var(--muted); }
.empty strong { display: block; color: var(--text); font-size: var(--fs-section); margin-bottom: 6px; font-weight: var(--w-medium); }
/* The quieter register: a fact, not a full empty state with an action. */
.empty-inline { padding: 14px 0; text-align: center; color: var(--muted); font-size: 12.5px; }

/* ------------------------------- pills ---------------------------------- */

/* 11.5px status text on a tinted chip needs 600 to hold; this is the one place
   the medium weight is doing legibility work rather than hierarchy. */
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border-radius: var(--r-pill);
  font-size: var(--fs-caption); font-weight: var(--w-medium); line-height: 1.65;
  background: var(--panel-2); color: var(--muted);
  border: 1px solid var(--border);
  white-space: nowrap;
}
.pill.green { background: var(--green-wash); color: var(--green-text); border-color: var(--green-line); }
.pill.red { background: var(--red-wash); color: var(--red-text); border-color: var(--red-line); }
.pill.amber { background: var(--amber-wash); color: var(--amber-text); border-color: var(--amber-line); }
.pill.blue { background: var(--accent-soft); color: var(--accent-text); border-color: var(--accent-line); }
.pill.purple { background: var(--purple-wash); color: var(--purple-text); border-color: var(--purple-line); }

/* ------------------------------ switches -------------------------------- */

/* An on/off control that saves on click. Overrides the generic button chrome:
   the track is the affordance, so the surrounding box would only add noise. */
.switch {
  align-items: center; gap: var(--sp-2);
  min-height: var(--ctl-h-sm); padding: 3px 4px;
  border-radius: var(--r-pill);
  color: var(--muted);
  font-size: var(--fs-label); font-weight: var(--w-medium);
}
.switch:hover { color: var(--text-dim); }
.switch.on { color: var(--green-text); }
/* Mixing towards --text lifts it in the dark theme and deepens it in the light
   one; either way hovering reads as more emphasis, not a different hue. */
.switch.on:hover { color: color-mix(in srgb, var(--green-text) 78%, var(--text)); }

.switch-track {
  position: relative; flex: none;
  width: 34px; height: 20px;
  border-radius: var(--r-pill);
  background: var(--panel-3);
  border: 1px solid var(--border-strong);
  transition: background var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease);
}
.switch-knob {
  position: absolute; top: 2px; left: 2px;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--faint);
  transition: transform var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.switch:hover .switch-track { border-color: var(--accent-line); }
.switch.on .switch-track {
  background: var(--green-wash-2);
  border-color: var(--green-line);
}
.switch.on .switch-knob { transform: translateX(14px); background: var(--green); }
.switch[aria-busy="true"] { cursor: progress; opacity: .72; }

@media (prefers-reduced-motion: reduce) {
  .switch-track, .switch-knob { transition: none; }
}

.flag {
  width: 20px; height: 15px; border-radius: var(--r-xs); object-fit: cover;
  vertical-align: -2px; background: var(--panel-2); flex: none;
  box-shadow: 0 0 0 1px var(--hairline);
}
.country { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }

/* ------------------------------- filters -------------------------------- */

/*
 * Stable columns, so reaching for Country becomes muscle memory instead of a
 * re-find. A flex wrap put nothing on line two under anything on line one, and
 * moved its own wrap point whenever a facet list changed a label's width.
 */
/*
 * One grid, declared once and used by both rows.
 *
 * The search row was a flex row with a 260px box while the filters below were a
 * grid of ~183px tracks, so the two never lined up — the thing an operator
 * reaches for first sat in its own column system. Sharing the track definition
 * means every control's left edge falls on the same column at every width.
 */
.filter-bar { --filter-cols: repeat(auto-fill, minmax(170px, 1fr)); }

.filters, .filter-row {
  display: grid;
  grid-template-columns: var(--filter-cols);
  gap: var(--sp-2) var(--sp-3);
  align-items: end;
}
/*
 * Both selectors are (0,2,1) on purpose: `label.f` is (0,1,1), so a bare
 * `.filter-search` at (0,1,0) loses to it and the label keeps the 12px
 * `margin-bottom` every stacked form field gets. That margin is part of the
 * item's margin box, so it sized the grid row to 49px and left the search box
 * sitting at the top of it while the toggle centred 6px lower — the two on
 * different centre lines for a reason invisible in either rule on its own.
 */
.filters label.f, .filter-row label.f { display: flex; flex-direction: column; margin-bottom: 0; }
.filters > button { justify-self: start; align-self: end; }

/* Search, the toggle and the active-filter chips, always visible. */
.filter-row { align-items: center; margin-bottom: var(--sp-3); }
/* Two tracks: 170px is right for a facet dropdown and cramped for a search
   box. Its left edge still lands on column 1, which is the alignment that
   reads. */
.filter-search { grid-column: span 2; }
/* Natural width rather than a button stretched to a full track — it starts on
   column 3 either way. */
.filter-bar .filter-toggle { justify-self: start; }
/* Their own row: a chip list is as long as the filters that are set, and
   squeezing it into whatever track is left would truncate it. */
.filter-chips { grid-column: 1 / -1; }

/* The bar collapses at every width, not only on a phone: twelve controls plus a
   Reset button lay out at ~2170px of track against 1156px of content at 1440,
   i.e. ~135px of permanent chrome between the page head and the first row. */
.filter-bar:not(.open) .filters { display: none; }
.filter-toggle { display: inline-flex; align-items: center; gap: var(--sp-2); }
.filter-toggle .count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: var(--r-pill);
  background: var(--accent-fill); color: var(--on-accent);
  font-size: var(--fs-caps); font-weight: var(--w-medium);
}

/* The chips are what make collapsing safe: dropping one filter costs open,
   find, Any, close without them. */
.filter-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.filter-chips:empty { display: none; }
.filter-chip { gap: 4px; padding-right: 3px; }
.filter-chip .chip-x {
  min-height: 0; width: 18px; height: 18px; padding: 0; flex: none;
  border: 0; background: none; color: inherit;
  font-size: var(--fs-caption); line-height: 1; border-radius: var(--r-pill);
}
.filter-chip .chip-x:hover { background: var(--accent-soft-2); border: 0; color: inherit; }

/* ------------------------------ utilities ------------------------------- */

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/*
 * Relative, deliberately not a token. An absolute 12px pinned a phone number a
 * pixel below the email beside it and refused to follow the density switch or
 * the phone breakpoint; 0.92em is an optical correction for the mono stack's
 * larger x-height and lands it at 11.96px inside the 13px table.
 */
.mono { font-family: var(--mono); font-size: 0.92em; font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); }
.small { font-size: 0.92em; }
.nowrap { white-space: nowrap; }
.right { text-align: right; }
.sep { height: 1px; background: var(--border); margin: var(--sp-4) 0; border: 0; }
/* A literal value beside a copy button, not a heading. */
.value-block { padding: 10px var(--sp-3); background: var(--panel-2); border-radius: var(--r-sm); word-break: break-all; }
.tnum { font-variant-numeric: tabular-nums; }

/* max-width is ignored on a td in auto table layout, so clamp an inner span. */
.truncate {
  display: inline-block; max-width: 220px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  vertical-align: bottom;
}
td.truncate > * { max-width: 100%; }

pre.code {
  background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--r);
  padding: 13px; overflow: auto; font-family: var(--mono); font-size: 12px;
  max-height: 340px; margin: 0; line-height: 1.6;
  tab-size: 2;
}

/* -------------------------------- bars ---------------------------------- */

.bar-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: center; margin-bottom: 9px; }
.bar-label { display: flex; align-items: center; gap: 7px; font-size: var(--fs-dense); min-width: 0; }
.bar-label .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-track { height: 6px; background: var(--panel-3); border-radius: var(--r-pill); overflow: hidden; margin-top: 5px; }
/* Flat: the fill's paint box *is* the datum, so a gradient made the mark's
   colour encode a second, meaningless variable — a short bar showed one end of
   the ramp and a full-width one the other. */
.bar-fill {
  height: 100%; border-radius: var(--r-pill);
  background: var(--accent-fill);
  transform-origin: left;
  animation: bar-in var(--dur-3) var(--ease-out) both;
}
@keyframes bar-in { from { transform: scaleX(0); } to { transform: scaleX(1); } }

.spark { display: flex; align-items: flex-end; gap: 2px; height: 96px; }
/* The columns are buttons so touch and keyboard can read their values; strip
   the button chrome back to a bare bar. */
.spark .col {
  flex: 1 1 0; min-width: 2px;
  background: var(--accent-fill);
  border-radius: var(--r-xs) var(--r-xs) 0 0;
  opacity: .8;
  transition: opacity var(--dur-1) var(--ease);
  animation: col-in var(--dur-3) var(--ease-out) both;
}
/* Hover reads as "this one" rather than "the render changed". */
.spark .col:hover, .spark .col:focus-visible { opacity: 1; }
@keyframes col-in { from { transform: scaleY(0); transform-origin: bottom; } to { transform: scaleY(1); } }

/* The chart card is paired with a list card of unknown length, so let the plot
   grow into whatever height the row ends up being instead of leaving a void. */
.card.stretch { display: flex; flex-direction: column; }
.chart-flex { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.chart-flex .spark { flex: 1 1 auto; min-height: 96px; }

.chart-axis {
  display: flex; justify-content: space-between; gap: 10px;
  margin-top: 9px; font-variant-numeric: tabular-nums;
}
/* Rewritten on every pointer move, so its digits have to hold still. */
.chart-readout {
  margin-top: 8px; padding-top: 9px;
  border-top: 1px solid var(--border);
  font-size: 12.5px; min-height: var(--ctl-h);
  font-variant-numeric: tabular-nums;
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
}
@media (max-width: 860px) {
  /* Fat-finger targets: a 2px bar is not tappable. */
  .spark { gap: 3px; height: 110px; }
  .spark .col { min-width: 6px; }
}

/* -------------------------------- modal --------------------------------- */

.modal-backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: var(--overlay);
  backdrop-filter: var(--scrim-blur);
  display: flex; align-items: flex-start; justify-content: center;
  padding: var(--sp-7) var(--sp-5);
  overflow-y: auto; overscroll-behavior: contain;
  animation: fade-in var(--dur-2) var(--ease-out) both;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

/* A 620px dialog being rounder than a 1100px card is backwards; --r-lg is the
   panel-sized radius and the modal is a panel. */
.modal {
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  width: 100%; max-width: 620px;
  box-shadow: var(--sh-4);
  animation: modal-in var(--dur-3) var(--ease-out) both;
}
@keyframes modal-in {
  from { opacity: 0; transform: translateY(14px) scale(.985); }
  to { opacity: 1; transform: none; }
}
.modal.wide { max-width: 940px; }

/* Dialogs leave the way they arrived, instead of being present then absent
   between frames. */
.modal-backdrop.is-closing { animation: fade-out var(--dur-1) var(--ease) both; }
@keyframes fade-out { to { opacity: 0; } }
.modal-backdrop.is-closing .modal { animation: modal-out var(--dur-1) var(--ease) both; }
@keyframes modal-out { to { opacity: 0; transform: translateY(6px) scale(.99); } }

/* One inset for all three parts; the vertical padding stays per part. */
.modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-4); border-bottom: 1px solid var(--border);
}
.modal-head h2 {
  margin: 0; font-size: var(--fs-heading); line-height: var(--lh-heading);
  letter-spacing: var(--tr-title); font-weight: var(--w-medium); min-width: 0;
}
.modal-body { padding: var(--sp-4); }
.modal-foot {
  display: flex; justify-content: flex-end; gap: var(--sp-2); flex-wrap: wrap;
  padding: 14px var(--sp-4); border-top: 1px solid var(--border);
  background: var(--tint-1);
  border-radius: 0 0 calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px);
}
/* Pulled back so a 34px box does not crowd the head's 16px inset. */
.close-x { margin-right: -5px; font-size: 19px; }

/* ---------------------------- views + columns --------------------------- */

/* One "this is on" treatment. aria-pressed is the state carrier, so the look an
   operator learns on the Views button is the same look in the columns dialog. */
.btn-view.is-active, button[aria-pressed="true"] {
  border-color: var(--accent-line);
  background: var(--accent-soft);
  color: var(--accent-text-hi);
}
.btn-view.is-active:hover, button[aria-pressed="true"]:hover { background: var(--accent-soft-2); border-color: var(--accent); }

/* One column, not two: the list order *is* the table order, so a grid that
   wrapped into a second column would be showing an arrangement the table does
   not have. */
.col-list {
  display: flex; flex-direction: column; gap: 2px;
  max-height: min(56vh, 460px); overflow-y: auto;
  padding-right: var(--sp-1);
}
/* A real transition, so ↑/↓ is something the eye can follow rather than a
   teleport that has to be re-read. The reduced-motion block neutralises it. */
.col-row {
  display: flex; align-items: center; gap: var(--sp-1);
  min-height: var(--ctl-h); padding: 2px 6px 2px 2px;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  background: var(--panel);
  transition: transform var(--dur-2) var(--ease-out);
}
.col-row:hover { background: var(--panel-2); }
.col-row.is-dragging { opacity: .5; border-color: var(--accent-line); background: var(--panel-3); }

.col-grip {
  flex: none; width: 22px; text-align: center;
  color: var(--faint); font-size: 15px; line-height: 1;
  cursor: grab; user-select: none;
}
.col-row:active .col-grip { cursor: grabbing; }

.col-toggle {
  display: flex; align-items: center; gap: 10px;
  flex: 1; min-width: 0; min-height: var(--ctl-h); padding: 4px 6px;
  border-radius: var(--r-sm); cursor: pointer;
}
.col-toggle input { width: 17px; flex: none; }
.col-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.col-note { flex: none; }
.col-row.is-locked .col-toggle { cursor: default; opacity: .72; }

.col-moves { display: flex; gap: 2px; flex: none; }
.col-moves button { min-width: 28px; padding: 3px 6px; line-height: 1; }
.col-moves button:disabled { opacity: .3; cursor: default; }

/* Freeze. Quiet until it is on, then it is one of the few things on this
   dialog that changes how the table behaves rather than what it contains. */
.col-pin {
  flex: none; min-width: 74px; padding: 3px 8px; line-height: 1.5;
  font-size: var(--fs-caps); white-space: nowrap; opacity: .55;
}
.col-row:hover .col-pin, .col-pin:focus-visible { opacity: 1; }
/* Only what the generic aria-pressed rule above cannot express. */
.col-pin[aria-pressed="true"] { opacity: 1; }
.col-row.is-pinned { border-color: var(--accent-line); }

/* --------------------------- tables as cards ---------------------------- */

/**
 * Below 560px a 16-column table is a horizontal-scrolling maze. `.as-cards`
 * restacks each row into a labelled card: the `data-label` on every cell
 * becomes its own heading, so the same markup serves both layouts and nothing
 * is hidden from the phone.
 */
@media (max-width: 560px) {
  .table-wrap.as-cards { overflow: visible; background: none; }
  .table-wrap.as-cards table,
  .table-wrap.as-cards tbody,
  .table-wrap.as-cards tr,
  .table-wrap.as-cards td { display: block; width: auto; }

  .table-wrap.as-cards thead { display: none; }

  .table-wrap.as-cards tr {
    position: relative;
    margin: 0 10px 10px;
    padding: 6px 4px 4px;
    border: 1px solid var(--border);
    border-radius: var(--r);
    background: var(--panel-2);
  }
  .table-wrap.as-cards tr:first-child { margin-top: 10px; }
  .table-wrap.as-cards tbody tr:hover { background: var(--panel-2); }

  .table-wrap.as-cards td {
    display: grid;
    grid-template-columns: minmax(88px, 34%) minmax(0, 1fr);
    gap: 12px; align-items: center;
    /* Without this a pill or a badge stretches to fill the value column. */
    justify-items: start;
    padding: 7px 10px;
    border-bottom: 1px solid var(--border);
    text-align: left;
  }
  .table-wrap.as-cards td:last-child { border-bottom: 0; }
  /* One of the two surviving caps treatments: a label inside a container that
     offers no other separation from the value beside it. */
  .table-wrap.as-cards td::before {
    content: attr(data-label);
    color: var(--muted); font-size: var(--fs-caps); font-weight: var(--w-medium);
    text-transform: uppercase; letter-spacing: var(--tr-caps);
  }
  /* Cells with nothing to label (the row checkbox) should not reserve a column. */
  .table-wrap.as-cards td:not([data-label]) { display: block; }
  .table-wrap.as-cards td.num { text-align: left; }
  .table-wrap.as-cards td .truncate { max-width: 100%; }

  /* The select box rides in the corner instead of taking a whole row. */
  .table-wrap.as-cards td.cell-select {
    position: absolute; top: 6px; right: 8px;
    padding: 0; border: 0; width: auto;
  }
  .table-wrap.as-cards .cell-tick { min-width: var(--tap); min-height: var(--tap); }
  .table-wrap.as-cards td[data-label="Lead ID"] { padding-right: 52px; }
  .table-wrap.as-cards td.cell-actions { text-align: left; }

  .table-wrap.as-cards .row-actions { justify-content: flex-start; flex-wrap: wrap; }
  .table-wrap.as-cards .row-actions > button { flex: 1 1 auto; }

  /* The nested scroller has nothing to scroll once rows are stacked. */
  .table-wrap.as-cards.tall { max-height: none; overflow-y: visible; }

  /*
   * Card mode undoes the grid the desktop table is built on, and every part of
   * the frozen-column machinery goes with it: there is no sideways scroll to
   * anchor against, the cells are blocks rather than a row, and the inline
   * `left` offsets would push them off the card. `!important` because those
   * offsets are written as inline styles by the view.
   */
  .table-wrap.as-cards colgroup { display: none; }
  .table-wrap.as-cards table.table-fixed { table-layout: auto; }
  .table-wrap.as-cards th.is-pinned,
  .table-wrap.as-cards td.is-pinned {
    position: static;
    left: auto !important;
    border-right: 0;
  }
  .table-wrap.as-cards .is-pin-edge { box-shadow: none; }
  /* One line with an ellipsis is a desktop-column rule; on a card the value has
     the full width of the screen and should use it. */
  .table-wrap.as-cards .leads-table td { white-space: normal; overflow: visible; }
  .table-wrap.as-cards .cell-lead .lead-name { white-space: normal; }
}

/* ----------------------------- action menu ------------------------------ */

/* Glyph spacing for the three '·' characters the trigger emits when it has no
   text — not typography, which is why it is a literal and why it is scoped so
   it can never reach a real word. */
.menu-trigger:not(.has-text) { letter-spacing: .09em; padding-left: 9px; padding-right: 9px; }

.menu-panel {
  position: fixed; z-index: 90;
  min-width: 176px; padding: var(--sp-1);
  background: var(--panel-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r);
  box-shadow: var(--sh-2);
  display: flex; flex-direction: column; gap: 1px;
  max-height: calc(100dvh - 32px); overflow-y: auto; overscroll-behavior: contain;
  animation: menu-in var(--dur-2) var(--ease-out) both;
}
@keyframes menu-in {
  from { opacity: 0; transform: translateY(-4px) scale(.98); }
  to { opacity: 1; transform: none; }
}
/* A menu grows from the edge it is anchored to, which is the only thing the
   animation is for. Class selector, so it beats the shorthand on .menu-panel. */
.menu-panel.is-above { animation-name: menu-in-up; }
@keyframes menu-in-up {
  from { opacity: 0; transform: translateY(4px) scale(.98); }
  to { opacity: 1; transform: none; }
}
.menu-trigger.has-text { gap: 6px; padding-left: 11px; padding-right: 9px; }
.menu-trigger .caret { font-size: 9px; opacity: .7; }
.menu-trigger-text { overflow: hidden; text-overflow: ellipsis; max-width: 15ch; }

.menu-item {
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr) auto;
  gap: 8px; align-items: center;
  justify-content: flex-start; text-align: left;
  width: 100%; min-height: var(--ctl-h);
  padding: 7px 11px;
  /* Concentric: the panel's radius minus its padding and border. */
  border-radius: calc(var(--r) - var(--sp-1) - 1px);
  font-weight: var(--w-regular); color: var(--text-dim); white-space: nowrap;
}
.menu-item:hover { background: var(--panel-3); color: var(--text); }
/* A fill rather than a move: a 1px shift inside a fixed popover reads as a
   glitch, but the item still has to acknowledge the press. */
.menu-item:active { background: var(--accent-soft); }
.menu-item.danger { color: var(--red-text); }
.menu-item.danger:hover { background: var(--red-wash); color: var(--red-text); }
.menu-item.is-checked { color: var(--text); }
.menu-check { color: var(--accent-hi); font-size: var(--fs-caps); text-align: center; }
.menu-label { overflow: hidden; text-overflow: ellipsis; }
.menu-hint { color: var(--faint); font-size: var(--fs-caption); font-weight: var(--w-regular); }

.menu-sep { height: 1px; margin: 5px 3px; background: var(--border); }
/* The other surviving caps treatment. */
.menu-heading {
  padding: 7px 11px 4px; color: var(--faint);
  font-size: var(--fs-caps); font-weight: var(--w-medium);
  text-transform: uppercase; letter-spacing: var(--tr-caps);
}

.row-actions { display: flex; gap: 6px; justify-content: flex-end; align-items: center; flex-wrap: nowrap; }

/* --------------------------------- logs --------------------------------- */

.log-list {
  display: flex; flex-direction: column; gap: 8px;
  max-height: 460px; overflow-y: auto; padding-right: 4px;
  overscroll-behavior: contain;
}
.log-entry {
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--panel-2); padding: 11px 13px;
  box-shadow: inset 3px 0 0 var(--border-strong);
}
.log-entry.is-error { box-shadow: inset 3px 0 0 var(--red); }
.log-entry-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
/* Prose inside a table keeps a prose ratio. */
.log-msg { flex: 1; min-width: 160px; font-size: var(--fs-dense); line-height: var(--lh-body); word-break: break-word; }

.log-details { margin-top: 10px; }
.log-details > summary {
  cursor: pointer; color: var(--muted); font-size: 12px;
  padding: 4px 0; user-select: none; list-style: none;
  min-height: 26px; display: flex; align-items: center; gap: 4px;
}
.log-details > summary::-webkit-details-marker { display: none; }
.log-details > summary::before { content: '▸'; transition: transform var(--dur-1) var(--ease); }
.log-details[open] > summary::before { transform: rotate(90deg); }
.log-details > summary:hover { color: var(--text); }

.log-block { margin-top: 10px; }
.log-block-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  font-size: var(--fs-caption); font-weight: var(--w-medium);
  color: var(--muted); margin-bottom: 6px;
}

/* -------------------------------- toasts -------------------------------- */

#toasts, #toasts-alert {
  position: fixed; right: var(--gutter); bottom: 18px; z-index: 100;
  display: flex; flex-direction: column; gap: 9px;
  max-width: min(400px, calc(100vw - var(--gutter) * 2));
  pointer-events: none;
}
.toast {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 11px 12px 11px 14px; border-radius: var(--r);
  font-size: var(--fs-dense);
  background: var(--panel-2); border: 1px solid var(--border-strong);
  box-shadow: var(--sh-2);
  pointer-events: auto;
  animation: toast-in var(--dur-3) var(--ease-out) both;
}
@keyframes toast-in {
  from { opacity: 0; transform: translateY(10px) scale(.97); }
  to { opacity: 1; transform: none; }
}
.toast.out { opacity: 0; transform: translateY(6px); transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease); }
/* The stripe as an inset shadow, so it follows the corner arc instead of
   mitring a coloured wedge into it. */
.toast.ok { box-shadow: var(--sh-2), inset 3px 0 0 var(--green); }
.toast.err { box-shadow: var(--sh-2), inset 3px 0 0 var(--red); }
.toast.info { box-shadow: var(--sh-2), inset 3px 0 0 var(--accent); }
/* Green versus red is the worst pair for a peripheral read, so the outcome is
   also stated in a glyph. ✓ / ✕ / i, never ⚠ or ℹ — those render
   emoji-presentation in Chrome on Windows, ignore `color`, and will not share
   a baseline with the text beside them. */
.toast-ico { flex: none; font-size: var(--fs-dense); line-height: 1.5; }
.toast.ok .toast-ico { color: var(--green-text); }
.toast.err .toast-ico { color: var(--red-text); }
.toast.info .toast-ico { color: var(--accent-text); }
.toast-msg { flex: 1; min-width: 0; }
.toast-x { color: var(--faint); font-size: 15px; }

/* ------------------------------- banners -------------------------------- */

.banner {
  padding: 11px 14px; border-radius: var(--r); font-size: var(--fs-dense);
  background: var(--amber-wash);
  border: 1px solid var(--amber-line);
  color: var(--amber-text);
}
.banner.info { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-text); }
/* Amber means caution and in progress. A caught exception and a rejected lead
   are neither. */
.banner.err { background: var(--red-wash); border-color: var(--red-line); color: var(--red-text); }

/*
 * Out of flow, so the surface never jolts under the pointer at the start and
 * end of a selection run. margin-bottom: 0 is load-bearing — .banner sets 14px
 * and it would otherwise sit 14px high.
 */
.bulk-bar {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 20px; z-index: 45;
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  width: auto; margin-bottom: 0;
  padding: 10px 12px 10px var(--sp-4);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--panel-3) 92%, transparent);
  backdrop-filter: var(--float-blur);
  border: 1px solid var(--border-strong);
  box-shadow: var(--sh-3);
  animation: toast-in var(--dur-3) var(--ease-out) both;
}
.bulk-count { font-weight: var(--w-medium); }
@media (max-width: 860px) {
  .bulk-bar {
    left: 12px; right: 12px; transform: none;
    bottom: max(12px, env(safe-area-inset-bottom));
    border-radius: var(--r-lg);
  }
  .bulk-bar > button { flex: 1 1 auto; }
  .bulk-count { flex: 1 1 100%; }
  /* So the two floating layers do not overlap. */
  body.has-selection #toasts, body.has-selection #toasts-alert { bottom: 84px; }
}
.banner strong { color: inherit; }

/* --------------------------------- tabs --------------------------------- */

.tabs {
  display: flex; gap: 2px; border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
.tab {
  padding: 9px 14px; min-height: var(--tap);
  align-items: center;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  color: var(--muted); font-weight: var(--w-medium);
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.tab:hover { color: var(--text); background: var(--panel-2); border-bottom-color: var(--border-strong); }
.tab.active { color: var(--text); border-bottom-color: var(--accent-hi); background: none; }

/* ------------------------------ skeletons ------------------------------- */

.loading { padding: var(--sp-6); text-align: center; color: var(--muted); }
/* The section head Integrations builds by hand, twice, at two values. */
.section-head { margin: var(--sp-5) 0 var(--sp-3); }

.skeleton {
  border-radius: var(--r-sm);
  background: linear-gradient(100deg, var(--panel-2) 22%, var(--panel-3) 42%, var(--panel-2) 62%);
  background-size: 260% 100%;
  animation: shimmer 1.5s linear infinite;
}
@keyframes shimmer { from { background-position: 160% 0; } to { background-position: -60% 0; } }

.sk-stat {
  height: 92px; border-radius: var(--r-lg);
  border: 1px solid var(--border); background: var(--panel);
  padding: var(--sp-3) var(--sp-4);
}
.sk-card { border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--panel); padding: var(--sp-4); }
/* The skeleton takes the edges of the thing it stands in for, rather than
   putting a bordered box inside a bordered box and doubling the border for the
   length of the request. */
.sk-table { border: 0; border-radius: inherit; overflow: hidden; }
.sk-table > * + * { border-top: 1px solid var(--border); }

/* --------------------------- view transition ---------------------------- */

/* Applied when the route changes, not after its data arrives. A whole-page
   transform layer buys nothing over an opacity fade. */
.view-enter { animation: view-in var(--dur-2) var(--ease-out) both; }
@keyframes view-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ========================================================================= */
/*                              RESPONSIVE                                   */
/* ========================================================================= */

/* Wide desktop: give the shell more room to breathe. The gutter is the one
   metric that is genuinely responsive, and the fixed layers read it too. */
@media (min-width: 1600px) {
  :root { --sidebar-w: 250px; --gutter: 42px; }
  .main { padding-top: 40px; padding-bottom: 80px; }
}

/* Laptop / small desktop. */
@media (max-width: 1280px) {
  .grid.cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Tablet: the 3-column dashboard rows are too cramped before this point, and
   cols-2 has had no rung at all between 1600 and 861 — which is exactly the
   width an iPad landscape and a half-screen browser use. */
@media (max-width: 1100px) {
  :root { --sidebar-w: 200px; --gutter: 20px; }
  .grid.cols-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid.cols-2 { grid-template-columns: minmax(0, 1fr); }
  .main { padding-top: 30px; padding-bottom: 64px; }
}

/* ------------------------- phone / small tablet -------------------------- */

@media (max-width: 860px) {
  :root { --tap: 44px; --gutter: 15px; --fs-title: 21px; }

  #app {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    grid-template-areas: "topbar" "main";
  }

  /* A real app bar, not a wrapped pile of links. */
  .topbar {
    grid-area: topbar;
    display: flex; align-items: center; gap: 10px;
    position: sticky; top: 0; z-index: 45;
    height: var(--topbar-h);
    padding: 0 10px 0 6px;
    background: var(--float-bg);
    backdrop-filter: var(--float-blur);
    border-bottom: 1px solid var(--border);
  }
  .topbar .brand { padding: 0; font-size: 15.5px; }
  .topbar-spacer { flex: 1; }
  .badge-slot { display: flex; align-items: center; gap: 8px; flex: none; }
  /* Both badges have moved into the app bar; the fixed row would only be an
     invisible box in the corner. */
  .corner-badges { display: none; }

  /* Sidebar becomes an off-canvas drawer. */
  .sidebar {
    position: fixed; inset: 0 auto 0 0; z-index: 60;
    width: min(280px, 84vw); height: 100dvh;
    grid-area: auto;
    padding: 18px 14px;
    border-right: 1px solid var(--border-strong);
    box-shadow: var(--sh-4);
    transform: translateX(-102%);
    visibility: hidden;
    transition: transform var(--dur-3) var(--ease-out), visibility var(--dur-3);
  }
  #app.nav-open .sidebar { transform: none; visibility: visible; }
  .nav-item { min-height: 46px; font-size: 15px; }
  .nav-item.active::before { left: -7px; }

  .nav-scrim {
    display: block;
    position: fixed; inset: 0; z-index: 55;
    background: var(--overlay);
    backdrop-filter: var(--scrim-blur);
    opacity: 0; visibility: hidden;
    transition: opacity var(--dur-2) var(--ease), visibility var(--dur-2);
  }
  #app.nav-open .nav-scrim { opacity: 1; visibility: visible; }

  .main { padding-top: 20px; padding-bottom: 72px; }

  /* The badge stops floating and rides in the app bar instead. */
  .version-badge {
    position: static; margin: 0;
    font-size: var(--fs-caps);
  }
  .version-badge .hash { display: none; }

  .page-head { align-items: flex-start; gap: var(--sp-3); }
  .head-actions { width: 100%; }
  .head-actions > * { flex: 1 1 auto; min-height: var(--tap); justify-content: center; }

  .grid.cols-2, .grid.cols-3, .grid.cols-4 { grid-template-columns: minmax(0, 1fr); }

  /* No border-radius here: .stat, .sk-card and .sk-stat were never included, so
     a skeleton visibly changed its corners at the moment its data landed. */
  .card { padding: 15px; }
  .stat { padding: 13px 15px; }
  .stat .value { font-size: 24px; }

  /* 16px is the threshold below which iOS Safari zooms the viewport on focus.
     The textarea is the largest field on the page and used to opt back out. */
  input, select, textarea { font-size: 16px; min-height: var(--tap); }
  .btn, button:where(:not(.bare)) { min-height: 38px; }
  .btn-sm { min-height: var(--ctl-h); }
  /* Both set an explicit height at class specificity, so min-height cannot
     reach them — dismiss a toast and close a sheet are the two most-tapped
     controls in the app. */
  .icon-btn, .icon-btn.sm { width: var(--tap); height: var(--tap); }
  label.f > span { font-size: 12.5px; }

  .filter-toggle { width: 100%; justify-content: space-between; min-height: var(--tap); }
  /* Narrower tracks on a phone — set on the token, so the search row and the
     filter panel stay on the same columns rather than drifting apart here. */
  .filter-bar { --filter-cols: repeat(auto-fill, minmax(150px, 1fr)); }
  /* The phone grid, which is a boxed panel rather than the desktop's bare row. */
  .filter-bar.open .filters {
    gap: 10px;
    padding: 14px;
    border: 1px solid var(--border); border-radius: var(--r-lg);
    background: var(--panel);
  }
  .filters label.f { width: auto; }
  .filters > button { grid-column: 1 / -1; min-height: var(--tap); justify-self: stretch; }
  /* Search takes the row on a phone; the toggle takes the next one. */
  .filter-search { grid-column: 1 / -1; }
  .filter-bar .filter-toggle { justify-self: stretch; }

  /* Modals become bottom sheets. */
  .modal-backdrop { padding: 0; align-items: flex-end; }
  .modal, .modal.wide {
    max-width: none; max-height: 94dvh;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    border-bottom: 0;
    display: flex; flex-direction: column;
    animation: sheet-in var(--dur-3) var(--ease-out) both;
  }
  @keyframes sheet-in {
    from { transform: translateY(28px); opacity: 0; }
    to { transform: none; opacity: 1; }
  }
  .modal-backdrop.is-closing .modal { animation: sheet-out var(--dur-2) var(--ease) both; }
  @keyframes sheet-out { to { transform: translateY(100%); } }
  .modal-head {
    position: sticky; top: 0; z-index: 2;
    background: var(--panel); border-radius: var(--r-lg) var(--r-lg) 0 0;
  }
  /* A mark that says "sheet", painted from its own token rather than borrowing
     an unrelated border colour. The drag it once implied is not built: Escape,
     the ×, the backdrop and the back gesture all already close this. */
  .modal-head::before {
    content: ""; position: absolute; top: 6px; left: 50%;
    width: 36px; height: 4px; margin-left: -18px;
    border-radius: var(--r-pill); background: var(--grabber);
  }
  .modal-head { padding-top: 18px; }
  .modal-body { overflow-y: auto; -webkit-overflow-scrolling: touch; flex: 1; padding: var(--sp-4); }
  .modal-foot { border-radius: 0; padding-bottom: max(14px, env(safe-area-inset-bottom)); }
  .modal-foot > button, .modal-foot > .btn { flex: 1 1 auto; min-height: var(--tap); }

  .grid.cols-2 > *, .grid.cols-3 > * { min-width: 0; }

  #toasts, #toasts-alert { right: 12px; left: 12px; bottom: max(12px, env(safe-area-inset-bottom)); max-width: none; }
  .toast { width: 100%; }

  .log-list { max-height: none; }
  .truncate { max-width: 190px; }

  table { font-size: 12.5px; }
  th, td { padding: 11px 10px; }
  /* A nested vertical scroller fights momentum scrolling on touch. */
  .table-wrap.tall { max-height: none; overflow-y: visible; }
  .leads-surface { max-height: none; }

  /* The sort control is 21px of text inside a 43px header cell. Grow its hit
     area to fill the cell without moving the label. */
  .th-sort { padding: 11px 5px; margin: -11px -5px; }
  .version-badge { min-height: 32px; }
}

@media (max-width: 480px) {
  :root { --gutter: 12px; --fs-title: 19px; }
  .main { padding-top: var(--sp-4); padding-bottom: 72px; }
  /* Share a row rather than stacking: three full-width buttons pushed the data
     itself below the fold. */
  .head-actions > * { flex: 1 1 auto; min-width: 0; font-size: 13px; padding-left: 8px; padding-right: 8px; }
  .filter-bar.open .filters { grid-template-columns: minmax(0, 1fr); }
  .truncate { max-width: 150px; }
}

/* Short landscape phones: the sheet should not eat the whole screen. */
@media (max-height: 480px) and (max-width: 860px) {
  .modal, .modal.wide { max-height: 100dvh; border-radius: 0; }
  .modal-head, .modal-foot { border-radius: 0; }
  .modal-head::before { display: none; }
}

/* --------------------------- user preferences ---------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

@media (prefers-contrast: more) {
  :root:not([data-theme="light"]) {
    --border: #3c4a72;
    --border-strong: #5a6a99;
    --muted: #a9b5d4;
    --faint: #8592b8;
  }
  :root[data-theme="light"] {
    --border: #aab5cb;
    --border-strong: #7d8aa6;
    --muted: #3a465f;
    --faint: #56637d;
  }
}

/* ---------------------------------- print -------------------------------- */

@media print {
  /* `:root[data-theme]` as well, so this still wins once a theme has been
     chosen — the light block would otherwise out-specify a bare `:root`. */
  :root, :root[data-theme] {
    --bg: #fff; --panel: #fff; --panel-2: #fff; --text: #000; --muted: #444;
    --row-alt: transparent; --row-hover: transparent;
  }
  body { background: #fff; color: #000; }
  .sidebar, .topbar, .version-badge, .theme-toggle, #toasts, .head-actions, .filters, .filter-toggle { display: none !important; }
  #app { grid-template-columns: 1fr; }
  .main { padding: 0; }
  .card, .stat { border: 1px solid #ccc; box-shadow: none; break-inside: avoid; }
  a { color: #000; text-decoration: underline; }
}
