/*
 * Paper — a small design system.
 * A warm, light, information-dense UI system: hairlines instead of shadows, one teal
 * accent, small type at high density. See paper-design-system.md.
 */

/* ── 1. Tokens ───────────────────────────────────────────────────────── */

:root {
  --bg: #fbfaf8;        --surface: #ffffff;   --strip: #f5f3ef;
  --line: #e6e2da;      --line-soft: #f0ece5; --input-line: #e0dbd2;
  --ink: #2b2926;       --body: #78736c;      --muted: #a8a29a;
  --accent: #0f766e;    --accent-dark: #0b5b55;
  --accent-bg: #eef5f4; --accent-line: #cfe2df;
  --warn: #b45309;      --warn-bg: #fdf7ee;   --warn-line: #f0e0c6;
  --danger: #b42318;    --danger-bg: #fdf3f2; --danger-line: #f6d9d5;
  --off: #c4bdb2;       --off-dot: #ded8ce;
  --on-accent: #ffffff;

  --col-check: 42px;    --col-source: 220px;
  --col-num: 96px;
  /* the export track is wide enough for its select plus the filter's Apply button;
     shared with the table, so widening it moves both in step */
  --col-time: 160px;    --col-export: 200px;
  /* a word and its dot below, its own select above — the select is the wider of the two */
  --col-parse: 150px;
  --filter-control-h: 28px;

  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* ── 2. Base ─────────────────────────────────────────────────────────── */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh; display: flex; flex-direction: column;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: .875rem;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

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

h1, h2, h3 { margin: 0; font-weight: 650; letter-spacing: -.02em; }

.page-title { font-size: 1.375rem; }
.page-head {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: 16px; min-height: 34px;
}
.page-head .spacer { margin-left: auto; }
.page-sub { font-size: .78125rem; color: var(--muted); }
/* a title and its subtitle stacked, when the head carries two lines rather than one */
.page-titles { display: flex; flex-direction: column; gap: 2px; }

code, pre, .mono { font-family: var(--font-mono); }

/* ── 3. Shell and top bar ────────────────────────────────────────────── */

.topbar { background: var(--strip); border-bottom: 1px solid var(--line); }
.topbar-inner {
  max-width: 1080px; margin: 0 auto; padding: 12px 24px;
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
}
.shell { flex: 1; width: 100%; max-width: 1080px; margin: 0 auto; padding: 22px 24px 48px; }

/* mirrors the top bar, and the column body keeps it at the foot of a short page */
.shell.is-narrow { max-width: 420px; padding-top: 56px; }

/* the favicon's mark, at the one size the bar has room for */
.brand { display: inline-flex; align-items: center; text-decoration: none; }
.brand-mark { display: block; flex: none; }
/* the mark carries no text to recolour, so the hover has to come from the mark itself */
.brand:hover .brand-mark { opacity: .85; }

/* Sections, beside the brand. Pills rather than buttons: a button here would read as
   heavier than Sign out at the other end of the same bar. */
.topnav { display: flex; align-items: center; gap: 2px; margin-left: 6px; }
.topnav-link {
  padding: 5px 10px; border-radius: 7px; font-size: .78125rem; font-weight: 500;
  color: var(--body); white-space: nowrap;
}
a.topnav-link:hover { background: var(--surface); color: var(--ink); }
.topnav-link.is-current { background: var(--surface); color: var(--ink); font-weight: 600; }

.topbar-user { font-size: .78125rem; color: var(--body); white-space: nowrap; }
a.topbar-user:hover { color: var(--ink); }
.topbar .spacer { margin-left: auto; }

/* ── 4. Panel and strips ─────────────────────────────────────────────── */

.panel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 12px; overflow: hidden;
}
.panel + .panel { margin-top: 16px; }

.panel-strip {
  background: var(--strip); padding: 12px 20px;
  font-size: .8125rem; color: var(--body);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.panel-strip.is-top    { border-bottom: 1px solid var(--line); }
.panel-strip.is-bottom { border-top: 1px solid var(--line); }
.panel-strip.is-error  {
  background: var(--danger-bg); border-bottom-color: var(--danger-line);
  color: var(--danger);
}
/* something to decide, not something wrong: warm rather than red, and the text runs on
   instead of sitting on one line — the band explains a choice, so it may be two deep */
.panel-strip.is-warn {
  background: var(--warn-bg); border-bottom-color: var(--warn-line); color: var(--warn);
  align-items: flex-start;
}
.panel-strip.is-warn > span:not(.strip-label) { max-width: 68ch; }
.panel-strip .spacer { margin-left: auto; }

/* The filter is its own panel above the list, but it runs on the table's tracks, so
   each control stands over the column it filters. Cells carry the table's 20px
   gutters, the row itself none. */
.filter-panel { display: block; }
/* no strip of its own: the row is the whole panel, and its bed is deep enough to
   stand the panel exactly as tall as the summary band above it */
.filter-row {
  padding: 14px 0;
  display: grid; align-items: center;
  grid-template-columns:
    minmax(0, 1fr) var(--col-source) var(--col-time) var(--col-parse) var(--col-export);
}
.filter-cell { min-width: 0; padding: 0 20px; }
.filter-row .filter-cell.col-grow,
.filter-row .filter-cell.col-export { display: flex; align-items: center; gap: 8px; }
.filter-row .filter-cell.col-grow input { flex: 1 1 auto; }
/* Apply closes the row: the select takes what the button leaves, not the other way round */
.filter-row .filter-cell.col-export select { flex: 1 1 auto; }
/* an input, a select and a tom-select each size themselves differently — pin all
   three to one height so the row reads as a single line of controls */
.filter-row .filter-cell input,
.filter-row .filter-cell select,
.filter-row .filter-cell .ts-wrapper,
.filter-row .filter-cell .ts-control {
  width: 100%; min-width: 0; max-width: none;
  font-size: .8125rem; line-height: normal;
}
.filter-row .filter-cell input,
.filter-row .filter-cell select,
.filter-row .filter-cell .ts-control {
  height: var(--filter-control-h); padding: 0 10px;
}
.filter-row .filter-cell select { padding-right: 6px; }
.filter-row .filter-cell .ts-control {
  min-height: var(--filter-control-h); height: auto; padding-right: 26px;
}
/* the caret tom-select keeps inside the control is an <input> too — it must not
   take the sizing meant for the filter's own fields, or the control grows past the rest */
.filter-row .filter-cell .ts-control > input { height: auto; padding: 0; }

/* The export criteria: the same idea as the filter above the list — a row of controls that
   is the whole of its panel — on tracks of its own, since nothing below it is a table to
   stand over. Each control keeps a label, which the filter has no room for and this panel
   needs: a bare date field says nothing about which end of the window it is. */
.criteria-row {
  padding: 16px 0;
  display: grid; align-items: end; gap: 14px 0;
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
.criteria-cell { min-width: 0; padding: 0 20px; }
/* the sources picker stays on the row with the others but takes two of its four tracks:
   it holds however many names are chosen, and a quarter would set it wrapping at the second */
.criteria-cell.is-wide { grid-column: span 2; }
.criteria-cell .field-label { margin-bottom: 5px; }
/* a date input, a select and a tom-select each size themselves differently — pin them to
   one height so the row reads as a single line of controls, as the filter's does */
.criteria-cell input,
.criteria-cell select,
.criteria-cell .ts-wrapper,
.criteria-cell .ts-control {
  width: 100%; min-width: 0; max-width: none;
  font-size: .8125rem; line-height: normal;
}
.criteria-cell input,
.criteria-cell select { height: var(--filter-control-h); padding: 0 10px; }
.criteria-cell select { padding-right: 6px; }
.criteria-cell .ts-control {
  min-height: var(--filter-control-h); height: auto; padding: 0 26px 0 10px;
}
.criteria-cell .ts-control > input { height: auto; padding: 0; }

.strip-label {
  font-size: .65625rem; text-transform: uppercase; letter-spacing: .09em;
  font-weight: 700; color: var(--muted);
}
/* The share of an export a block was dealt. Two blocks are the same thing twice over, and
   this is the one line that tells them apart, so it takes the accent and a size of its own
   rather than the grey a label is set in. */
.strip-share {
  display: inline-block; padding: 1px 8px; border-radius: 6px;
  background: var(--accent-bg); border: 1px solid var(--accent-line); color: var(--accent);
  font-size: .875rem; font-weight: 700; white-space: nowrap;
}

.panel-strip.is-error .strip-label { color: var(--danger); }
.panel-strip.is-warn .strip-label { color: var(--warn); }

/* ── 5. Dense table ──────────────────────────────────────────────────── */

.table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: .84375rem; margin: 0; }
.table th {
  text-align: left; font-weight: 600; font-size: .65625rem;
  letter-spacing: .09em; text-transform: uppercase; color: var(--muted);
  padding: 9px 20px; border-bottom: 1px solid var(--line); white-space: nowrap;
}
.table td {
  padding: 11px 20px; border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover td { background: var(--strip); }
/* the totals under a full listing: a reading of the column above, not another row in it */
.table tfoot td {
  padding: 11px 20px; border-top: 1px solid var(--line); background: var(--strip);
  font-weight: 650; color: var(--ink);
}
.table tfoot td.is-time { color: var(--ink); }

/* the identifying column: machine strings, so monospace */
.table td.is-data {
  font-family: var(--font-mono); font-size: .8125rem;
  font-weight: 500; color: var(--ink);
}
.table td.is-data a { color: var(--ink); }
.table td.is-data a:hover { color: var(--accent); }
.table td.is-source { font-size: .78125rem; color: var(--body); }
.table td.is-source a { color: var(--body); }
.table td.is-source a:hover { color: var(--ink); }
.table td.is-meta  { font-size: .78125rem; color: var(--body); }
.table td.is-mono  { font-family: var(--font-mono); font-size: .78125rem; color: var(--muted); }
.table td.is-time  { text-align: right; color: var(--muted); font-size: .78125rem; white-space: nowrap; }
.table td.is-when  { color: var(--muted); font-size: .78125rem; white-space: nowrap; }
.table .age { color: var(--off); font-size: .71875rem; }
.table .truncate {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* exported: struck through and quieted down, in the list and on the detail page */
.table td.is-exported, .table td.is-exported a { text-decoration: line-through; color: var(--muted); }
.table td.is-exported a:hover { color: var(--body); }
.value.is-exported { text-decoration: line-through; color: var(--muted); }

/* the fixed tracks, shared by the table and by the filter row standing over it */
.col-grow   { width: auto; }
.col-source { width: var(--col-source); }
.col-time   { width: var(--col-time); }
.col-parse  { width: var(--col-parse); }
.col-export { width: var(--col-export); }
.col-check  { width: var(--col-check); }
.col-line   { width: 58px; }
/* the sources table carries four counts beside its bar — narrower tracks than the list's,
   so the bar keeps enough room to be read */
.col-num    { width: var(--col-num); }
.table th.col-num { text-align: right; }
/* the export's per-source table has nothing but counts beside the name, and a source name
   is short: the counts take wider tracks so the name column, the only elastic one, keeps
   no more width than it reads in. Set on that table alone — the sources listing carries
   four counts and two fixed columns besides, and tracks this wide would push its last
   two columns out past the panel edge (the table is laid out fixed, so they are cut, not
   squeezed). */
.table.is-breakdown { --col-num: 220px; }
/* the count the page is for: what this source can actually hand out, carried at the weight
   of the number on the button rather than in the muted type the other counts share */
.table.is-sources td.is-lead {
  color: var(--ink); font-weight: 650; font-size: .84375rem;
  font-variant-numeric: tabular-nums;
}
.table.is-sources th.is-lead { color: var(--ink); }
.col-act    { width: 96px; }
/* a row's own button, right up against the panel edge; button_to wraps it in a form */
.table td.is-act { text-align: right; }
.table td.is-act form { display: inline-block; margin: 0; }
.table th.col-check, .table td.col-check { padding-right: 0; }

/* The parsing overview: four counts to a row and two actions after them, so the source gives
   up the width they need — it is a name to recognize, not a value to read in full, and what
   will not fit is one hover away.

   The actions track is sized for its worst case, both buttons carrying a four-figure count,
   and split down the middle into a slot apiece — Review on the left, Export on the right —
   held open whether or not the row has that action. A row with one button used to slide it
   to the panel edge, under the Exports of the rows above it, and a warm button standing in
   the accent one's place is misread before the label is. */
.table.is-parsing { --col-export: 240px; }
/* The closing line set in the head's own type — same size, same tracking, same small caps —
   so the table is bracketed by two lines of one kind with the rows between them. Left in
   the ink the totals row carries: it is a reading of the columns, not a second label for
   them, and the type alone is what pairs it with the head. */
.table.is-parsing tfoot td,
.table.is-sources tfoot td {
  font-size: .65625rem; font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase;
}
/* The totals themselves are not a label and do not take the label's size: they are the
   readings the column ends on, set in the type the band above the table counts in. Figures
   of one width, so the column of them lines up under the rows it sums. */
.table.is-parsing tfoot td.col-num,
.table.is-sources tfoot td.col-num {
  font-size: .9375rem; font-weight: 650; letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.act-slots { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.act-slots .slot { display: flex; min-width: 0; }
/* the button takes its slot whole, so a count of one figure and a count of four end on the
   same edge and the column of buttons is a column */
.act-slots .slot > a {
  flex: 1 1 auto; text-align: center;
  font-variant-numeric: tabular-nums;
}
/* the header goes with them, as the counts' headers go with the counts */
.table.is-parsing th.col-export { text-align: right; }

.dash { color: var(--muted); }
.hint-link { font-size: .75rem; }

/* ── 5a. Duplicates ──────────────────────────────────────────────────── */

/* no filter row stands over this table, so its tracks answer to nothing but the rows: the
   fixed columns are cut back and the data column takes what they give up — each row here
   carries a long value and a marker beside it, and that is what the room is for */
.table.is-duplicates {
  --col-source: 150px;
  --col-time:   120px;
  --col-export: 130px;
}

/* a value and its marker sharing one cell: the value takes the room, the marker keeps
   its own width so a long value cannot squeeze it out of sight */
.cell-pair { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* the marker hugs the value instead of lining up at the far edge of the column: it says
   something about that value, and a wide column would leave it stranded */
.cell-pair .truncate { flex: 0 1 auto; min-width: 0; }
.cell-pair .badge, .cell-pair .tag { flex: none; }
/* a cell may hold two actions, one a link and one a button_to form — the form is a flex
   item of its own, and the two are put on one height for the same reason as in a strip */
.cell-pair form { margin: 0; flex: none; }
.cell-pair .btn-primary, .cell-pair .btn-ghost { line-height: 1.5; }
.cell-pair .btn-primary { border: 1px solid transparent; }

/* what a group's values have in common, so that where the ground stops is where they part.
   Every row of a group shares the same opening and the column is monospace, so the grounds
   come out the same width and stack into one block: the eye reads its edge, not each row's.
   No horizontal padding — a character of shift either side would ragged that edge.
   Nothing is said in the text colour: that belongs to the link, which turns accent on hover.
   Left on exported rows too, struck through and all — a gap in the block would read as a row
   sharing less than its neighbours, which is the one thing it must not say. */
.table.is-duplicates td.is-data .is-same {
  background: var(--warn-bg); padding: 2px 0;
}

/* how many records say this — a count, quiet until it matters */
.badge {
  display: inline-block; padding: 1px 6px; border-radius: 6px;
  background: var(--warn-bg); border: 1px solid var(--warn-line); color: var(--warn);
  font-family: var(--font-sans); font-size: .6875rem; font-weight: 700;
  letter-spacing: .02em; white-space: nowrap;
}

/* which side of a group a row is on */
.tag {
  display: inline-block; padding: 1px 6px; border-radius: 6px;
  background: var(--strip); border: 1px solid var(--line); color: var(--muted);
  font-family: var(--font-sans); font-size: .625rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .07em; white-space: nowrap;
}
tr:has(input:not(:checked)) .tag { background: var(--accent-bg); border-color: var(--accent-line); color: var(--accent); }
/* held together by the cut, not by the value: the one tag that has to read as a caution,
   and it keeps that reading whether or not the row is ticked */
.tag.is-loose, tr:has(input:not(:checked)) .tag.is-loose {
  background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn);
}

/* a table meant for reading values against each other: the identifying column wraps instead
   of ending in an ellipsis, since the difference between two rows can sit anywhere in it */
.table.is-comparing td.is-data,
.table.is-comparing td.is-source { white-space: normal; }
.table.is-comparing td.is-data a, .table.is-comparing td.is-source a,
.table.is-comparing td.is-source > span { overflow-wrap: anywhere; }
/* marks ride at the top of a value that may run to several lines, and keep their order
   however many of them a row carries */
.table.is-comparing .cell-pair { align-items: flex-start; flex-wrap: wrap; }
.table.is-comparing .cell-pair > :first-child {
  flex: 1 1 60%; min-width: 0; overflow-wrap: anywhere;
}
.table.is-comparing .cell-pair .tag { margin-top: 2px; }

/* two mutually exclusive views of the same data, one of them current */
.toggle { display: inline-flex; align-items: center; gap: 2px; }
.toggle-option {
  padding: 3px 9px; border-radius: 7px; font-size: .75rem; font-weight: 500;
  color: var(--body); white-space: nowrap;
}
a.toggle-option:hover { background: var(--surface); color: var(--ink); }
.toggle-option.is-current {
  background: var(--surface); border: 1px solid var(--line);
  color: var(--ink); font-weight: 600; padding: 2px 8px;
}

/* a short list of radio choices, each with its rule spelled out under the label */
.choices { display: flex; flex-direction: column; gap: 10px; }
.choice { display: flex; align-items: flex-start; gap: 9px; cursor: pointer; }
.choice input[type="radio"] {
  flex: none; width: 14px; height: 14px; margin: 3px 0 0; accent-color: var(--accent);
}
.choice-label { display: block; font-size: .84375rem; color: var(--ink); font-weight: 500; }
.choice-hint { display: block; font-size: .75rem; color: var(--muted); margin-top: 1px; }
.choice:hover .choice-label { color: var(--accent); }

/* the quiet half of a pairing: where the other record came from, when it arrived */
.pair-note { color: var(--muted); font-size: .71875rem; }
/* under the value it belongs to, not beside it — the column is too narrow for both */
.table td.is-source .pair-note { display: block; margin-top: 2px; }

/* a group's own row: no hover, no checkbox, just the value everything under it repeats.
   The band above it is a border in the panel's own colour rather than padding — padding
   would only make the strip taller, where this leaves a true gap that no row's hover
   reaches into, and collapsed borders let the thick one win over the hairline below. */
.table tr.group-head td {
  background: var(--strip); border-bottom-color: var(--line);
  border-top: 20px solid var(--surface);
  box-shadow: inset 0 1px 0 var(--line);
  padding-top: 8px; padding-bottom: 8px;
}
/* the first group opens the table: it wants the header's hairline, not a gap and a second
   line under it */
.table tbody tr.group-head:first-child td { border-top: 0; box-shadow: none; }
.table tr.group-head:hover td { background: var(--strip); }
/* one line, however long the value: it sits in a cell-pair, so the ellipsis falls on the
   value and the count and the note beside it keep their place */
.group-value { font-size: .8125rem; font-weight: 600; color: var(--ink); }

/* sortable headers: quiet by default, ink once they carry the sort */
.table th a.sort { color: var(--muted); text-decoration: none; white-space: nowrap; }
.table th a.sort:hover { color: var(--ink); }
.table th a.sort.is-active { color: var(--ink); }

/* ── 5b. Parsing ─────────────────────────────────────────────────────── */

/* counts on the parsing overview line up right the way the sources table's do */
.table td.col-num { text-align: right; }

/* the select-all box moved into the top strip once the table lost its header row */
.strip-check { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; }

/* one record per block: the record it was read from on top, the fields it broke into below.
   The check sits in its own narrow column so the bodies line up whatever the check does. */
.parse-item {
  display: grid; grid-template-columns: var(--col-check) minmax(0, 1fr);
  padding: 14px 20px; border-bottom: 1px solid var(--line-soft);
}
.parse-item:last-child { border-bottom: 0; }
.parse-check { padding-top: 2px; }
/* the validation queue has nothing to tick — its rows are saved by being changed — so the
   body takes the whole width rather than leaving the check's gutter empty */
.parse-item.is-plain { grid-template-columns: minmax(0, 1fr); }

/* the record's own text, wrapping in full — it is read against the fields under it, so it is
   not cut off at an ellipsis */
.parse-record {
  display: block; color: var(--body); font-size: .8125rem;
  overflow-wrap: anywhere; margin-bottom: 10px;
}
.parse-record:hover { color: var(--accent); }

/* on the batch page the record is read against a whole wall of field inputs, and those are
   themselves mono at .78125rem — so mono alone would not lift it out. Size, weight and the
   full ink colour do, and the extra space below sets it apart from the fields it broke into. */
.parse-record.is-lead {
  font-family: var(--font-mono); font-size: .9375rem; line-height: 1.5;
  font-weight: 500; color: var(--ink); margin-bottom: 12px;
}

/* the fields under the record: a row that wraps, each field as wide as its config asks for,
   so a narrow screen stacks them instead of scrolling.

   The width comes from the field's `max` (a character count) carried in `--field-ch`: the
   input is sized to fit about that many characters, clamped so a tiny max still clicks and a
   huge one does not run past the row. It is a hint for the interface only — the stored value
   can be longer. A field with no max gets `is-flex` and grows to fill what is left. */
.parse-fields { display: flex; flex-wrap: wrap; gap: 10px; }

/* the shared width rule, applied to whichever element is the flex item: the labelled wrapper
   on the record page, the bare input on the batch page */
.parse-field,
.parse-fields > .is-cell {
  flex: 0 1 calc((var(--field-ch, 24) + 4) * 1ch);
  min-width: 8ch; max-width: 100%;
}
.parse-field.is-flex,
.parse-fields > .is-cell.is-flex { flex: 1 1 22ch; }

/* one field on the record page: its name above the input, so a filled value still says which
   field it is — the placeholder only speaks while the field is empty */
.parse-field { min-width: 8ch; display: flex; flex-direction: column; gap: 5px; }
.parse-field .is-cell { width: 100%; }
.parse-field-label {
  font-size: .65625rem; text-transform: uppercase; letter-spacing: .09em;
  font-weight: 700; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* a field is an input, not text: editable in place, reading as a value rather than a form
   control until it is focused. Its placeholder carries the field's name. */
.is-cell {
  padding: 6px 10px; font-size: .78125rem; font-family: var(--font-mono);
  background: var(--surface); border-color: var(--input-line); border-radius: 7px;
}
/* a field the rules left empty: outlined so the eye lands on what needs a hand, without
   shouting — it is a blank to fill, not an error */
.is-cell.is-missing { border-color: var(--warn-line); background: var(--warn-bg); }
.is-cell.is-missing:focus { background: var(--surface); }
/* a field this source has no rule for: not editable, and it says so in its placeholder */
.is-cell.is-unruled {
  background: var(--strip); border-style: dashed; color: var(--muted); cursor: not-allowed;
}
/* a value that does not pass the check its field is held to. Red where an empty field is
   amber, and after it in the file so that it wins when a field is both — a required field
   left blank is a value that is wrong, not a blank waiting to be filled. */
.is-cell.is-invalid { border-color: var(--danger-line); background: var(--danger-bg); }
.is-cell.is-invalid:focus { background: var(--surface); }

/* what is wrong with the record, under the fields rather than under the cells. In words and
   not only a tooltip — on the validation queue it is the reason the row is on the screen at
   all — but across the whole row's width, because a cell is only as wide as its field's `max`
   asks for and a complaint threaded through four characters is four lines tall. Each is named
   by the field it is about, which is what ties it back to the cell that went red. */
.parse-problems {
  display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 9px;
  font-size: .6875rem; line-height: 1.4; color: var(--danger);
}
.parse-problem-field {
  text-transform: uppercase; letter-spacing: .09em; font-weight: 700; margin-right: 5px;
}

/* a record still missing a field, until it is ticked: a hairline down the block's edge
   rather than a wash across it, which would fight the inputs for attention */
.parse-item.is-incomplete { box-shadow: inset 2px 0 0 var(--warn-line); }
/* and the same hairline in red for a record holding a value it is not allowed to hold —
   after the rule above, so a record that is both reads as the louder of the two */
.parse-item.is-invalid { box-shadow: inset 2px 0 0 var(--danger); }

/* a page of records at a dozen fields each is a long scroll, and the approve button sat at
   the end of it — the bar follows instead, carrying the selected count with it.

   `clip` rather than the panel's usual `hidden`: both round the corners off the strips, but
   `hidden` makes the panel a scroll container, and a sticky child of one has nothing to
   stick to. Only this panel, so the rest keep the plain rule. */
.panel.has-sticky-foot { overflow: clip; }
.panel-strip.is-bottom.is-sticky { position: sticky; bottom: 0; z-index: 2; }

/* ── 6. Status dot ───────────────────────────────────────────────────── */

.dot {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  font-size: .71875rem; font-weight: 600; color: var(--accent);
}
.dot::before { flex: none; }
.dot::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
}
.dot.is-off { color: var(--off); }
.dot.is-off::before { background: var(--off-dot); }
/* done, but worth a second look — the same caution the loose tag carries */
.dot.is-warn { color: var(--warn); }
.dot.is-warn::before { background: var(--warn); }

/* ── 7. Buttons ──────────────────────────────────────────────────────── */

.btn-primary {
  display: inline-block; background: var(--accent); color: var(--on-accent);
  border: 0; border-radius: 8px; padding: 7px 15px;
  font-size: .8125rem; font-weight: 500; font-family: inherit;
  text-decoration: none; white-space: nowrap; cursor: pointer;
}
.btn-primary:hover { background: var(--accent-dark); color: var(--on-accent); }

.btn-ghost {
  display: inline-block; background: var(--surface);
  border: 1px solid var(--input-line); color: #4a4640;
  border-radius: 8px; padding: 8px 14px; font-size: .78125rem;
  font-weight: 500; font-family: inherit; text-decoration: none;
  white-space: nowrap; cursor: pointer;
}
.btn-ghost:hover { border-color: var(--muted); color: var(--ink); }
.btn-ghost.is-danger { color: var(--danger); }
.btn-ghost.is-danger:hover { border-color: var(--danger); color: var(--danger); }
/* Two actions standing side by side in one cell, told apart by colour rather than by
   reading them: the accent is the step that hands something over, the warm one the step
   that still wants a person. The same two colours the counters above them are set in. */
.btn-ghost.is-accent { color: var(--accent); }
.btn-ghost.is-accent:hover { border-color: var(--accent); color: var(--accent-dark); }
.btn-ghost.is-warn { color: var(--warn); }
.btn-ghost.is-warn:hover { border-color: var(--warn); color: var(--warn); }
.btn-sm { padding: 6px 13px; font-size: .75rem; }
/* A button repeated down every row of a table. Smaller than the one a strip carries — a
   column of them should read as a column of counts with a way to act on each, not as a
   stack of buttons — and still wide enough for a four-figure count, which is why the type
   comes down with the padding rather than the padding alone. */
.btn-xs { padding: 4px 9px; font-size: .6875rem; border-radius: 7px; }
/* Strips and action groups stand the two kinds of button side by side, and left alone they
   do not match: button_to renders a <button>, whose UA font shorthand resets line-height,
   while a link_to button is an <a> on the body's 1.5; and .btn-primary carries no border
   where .btn-ghost has a hairline. Both are put back here, scoped to the places that set
   buttons in a row so the button keeps its own metrics elsewhere — the filter row measures
   itself against them. */
.panel-strip .btn-primary, .panel-strip .btn-ghost,
.actions .btn-primary, .actions .btn-ghost { line-height: 1.5; }
.panel-strip .btn-primary, .actions .btn-primary { border: 1px solid transparent; }

/* A select in a strip sizes to what it holds. The width above is for a field, where one
   control owns the row; in a strip it stands in a sentence with words either side of it. */
.panel-strip select {
  width: auto; max-width: 30ch; padding-top: 5px; padding-bottom: 5px;
  font-size: .8125rem;
}

/* a square the size of one filter control, carrying a glyph instead of a word */
.btn-icon {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: var(--filter-control-h); height: var(--filter-control-h);
  padding: 0; border: 0; border-radius: 8px; cursor: pointer;
  background: var(--accent); color: var(--on-accent);
}
.btn-icon svg { width: 16px; height: 16px; }

/* The same square where the glyph is one of several things on a band rather than the one
   thing to do: an outline until it is pointed at, so a row of them reads as a row of tools
   and not a row of decisions. */
.btn-icon.is-ghost {
  background: transparent; color: var(--body);
  border: 1px solid var(--line);
}
.btn-icon.is-ghost:hover {
  background: var(--surface); border-color: var(--accent-line); color: var(--accent);
}
/* done, and said without a word to say it with: the button lights up and settles back */
.btn-icon.is-done, .btn-icon.is-ghost.is-done {
  background: var(--accent-bg); border-color: var(--accent-line); color: var(--accent);
}
.btn-icon:hover { background: var(--accent-dark); color: var(--on-accent); }
.btn-icon svg { display: block; width: 15px; height: 15px; }
.btn-icon.is-ghost {
  background: var(--surface); border: 1px solid var(--input-line); color: var(--body);
}
.btn-icon.is-ghost:hover { border-color: var(--muted); color: var(--ink); }

.btn-primary:disabled, .btn-ghost:disabled,
.btn-primary.is-disabled, .btn-ghost.is-disabled { opacity: .5; pointer-events: none; }
.is-current { color: var(--ink); font-weight: 600; }

.actions { display: flex; align-items: center; gap: 8px; }
/* a lone action closing a panel: centred, since there is no row of fields above it for it
   to line up with — it belongs to the panel rather than to a last line of it */
.actions.is-centered { justify-content: center; }

/* ── 8. Forms ────────────────────────────────────────────────────────── */

.field { padding: 16px 20px; border-bottom: 1px solid var(--line-soft); }
/* the last row of a table drops its rule, so the field following one needs its own */
.table + .field { border-top: 1px solid var(--line); }
.field:last-of-type { border-bottom: 0; }
/* a field the one under it belongs with: no rule between them, and no bed of its own
   either — the gap is the next field's, so the two read as one thing in two parts */
.field.is-joined { border-bottom: 0; padding-bottom: 0; }

/* Something worth having and not worth the room it takes every time: closed, it is one word
   in the hand of a field label; open, whatever it holds stands under it. The caret turns
   rather than changing, so the word does not move when it is opened. */
.disclose > summary {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  font-size: .65625rem; text-transform: uppercase; letter-spacing: .09em;
  font-weight: 700; color: var(--muted); list-style: none;
}
.disclose > summary::-webkit-details-marker { display: none; }
.disclose > summary::before {
  content: ""; flex: none; width: 0; height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent;
  transition: transform .12s ease;
}
.disclose[open] > summary::before { transform: rotate(90deg); }
.disclose > summary:hover { color: var(--ink); }
.disclose[open] > summary { margin-bottom: 7px; }
.field-label {
  display: block; font-size: .65625rem; text-transform: uppercase;
  letter-spacing: .09em; font-weight: 700; color: var(--muted);
  margin-bottom: 7px;
}
.hint { font-size: .75rem; color: var(--muted); margin-top: 7px; }

/* A paste field carrying a gutter of line numbers. The border and the focus ring move to
   the wrapper so the two read as one control; the field inside gives both up. The gutter
   repeats the field's font, size and line height exactly — anything else and the numbers
   drift away from the rows they count. */
.code-field {
  display: flex; align-items: stretch;
  background: var(--surface); border: 1px solid var(--input-line);
  border-radius: 8px; overflow: hidden;
}
.code-field:focus-within {
  border-color: rgba(15, 118, 110, .45); box-shadow: 0 0 0 3px rgba(15, 118, 110, .14);
}
.line-gutter {
  flex: none; min-width: 40px; overflow: hidden;
  padding: 9px 8px 9px 10px; border-right: 1px solid var(--line-soft);
  background: var(--strip); color: var(--muted);
  font-family: var(--font-mono); font-size: .8125rem; line-height: 1.6;
  text-align: right; white-space: pre; user-select: none; -webkit-user-select: none;
}
.code-field textarea {
  flex: 1 1 auto; min-width: 0;
  border: 0; border-radius: 0; background: transparent;
}
.code-field textarea:focus { box-shadow: none; }

/* Syntax colour. The <pre> underneath carries the colours and the field's own text is made
   transparent over it, so what is read is the copy and what is typed into is still a plain
   textarea. The two only stay aligned while every measure is shared, which is why the font,
   size, line height and padding are set on both here rather than left to either side. */
.code-stack { position: relative; display: flex; flex: 1 1 auto; min-width: 0; }
.code-hl, .code-stack textarea {
  padding: 9px 12px; font-family: var(--font-mono); font-size: .8125rem; line-height: 1.6;
  white-space: pre; tab-size: 2;
}
.code-hl {
  position: absolute; inset: 0; margin: 0; overflow: hidden;
  color: var(--ink); pointer-events: none;
}
.code-stack textarea {
  position: relative; flex: 1 1 auto; min-width: 0;
  color: transparent; caret-color: var(--ink);
}
/* Selection has to stay see-through: the text it covers is the transparent one, and the
   colours it needs to leave readable are painted below it. */
.code-stack textarea::selection { background: rgba(15, 118, 110, .22); }

/* Roles rather than languages — a comment looks the same whether it is the file's or a
   rule's. No new hue enters the palette: the accent carries literals, the warn tone carries
   patterns, and everything else is ink, body and muted. Names the rules are handed (data,
   source) share the key's weight; they never share its place, so the two do not collide. */
.hl-comment  { color: var(--body); font-style: italic; }
.hl-key      { color: var(--ink); font-weight: 600; }
.hl-punct    { color: var(--muted); }
.hl-literal  { color: var(--accent); }
.hl-regex    { color: var(--warn); }
.hl-keyword  { color: var(--accent-dark); font-weight: 600; }
.hl-argument { color: var(--ink); font-weight: 600; }

input[type="text"], input[type="password"], input[type="search"], input[type="date"],
textarea, select {
  background: var(--surface); border: 1px solid var(--input-line);
  border-radius: 8px; padding: 9px 12px; font-size: .84375rem;
  font-family: inherit; color: var(--ink); width: 100%;
}
textarea { min-height: 120px; resize: vertical; line-height: 1.6; }
textarea.mono { font-size: .8125rem; }
textarea[readonly] { background: var(--strip); color: var(--ink); }

/* The header of the exported file, shown as the file will carry it. Dressed as the readonly
   field under it so the two read as one thing in two parts, but it is text and not a
   control: the lines it describes are what gets copied, and it has no part in that. Its own
   line breaks and spacing stand, and a field line wider than the panel scrolls rather than
   wrapping — wrapped, it would no longer look like the one line it is. */
.legend {
  margin: 0; padding: 9px 12px; overflow-x: auto;
  background: var(--strip); border: 1px solid var(--input-line); border-radius: 8px;
  color: var(--ink); font-size: .8125rem; line-height: 1.6;
}
input.is-short { max-width: 140px; }
input.is-tiny { max-width: 76px; padding: 6px 10px; }

/* controls sitting in a band size to their content instead of filling the row */
.panel-strip input, .panel-strip select {
  width: auto; max-width: 220px; padding: 6px 10px; font-size: .8125rem;
}
.panel-strip input.is-tiny { max-width: 76px; }

input[type="checkbox"] {
  display: block; width: 14px; height: 14px; margin: 0;
  accent-color: var(--accent); cursor: pointer;
}

input:focus, textarea:focus, select:focus {
  outline: none; border-color: rgba(15, 118, 110, .45);
  box-shadow: 0 0 0 3px rgba(15, 118, 110, .14);
}
.btn-primary:focus-visible, .btn-ghost:focus-visible, .btn-icon:focus-visible, a:focus-visible {
  outline: none; box-shadow: 0 0 0 3px rgba(15, 118, 110, .18);
}
::placeholder { color: var(--muted); }

/* ── 9. Odds and ends ────────────────────────────────────────────────── */

/* A heading over a group of panels, when one page carries several. Smaller than the page's
   own title, which names the page and stands alone above everything on it. */
.section-head {
  display: flex; align-items: baseline; gap: 10px;
  margin: 22px 0 8px;
}
/* One that follows a section already on the page stands further off it than the blocks of
   that section stand from each other — the step up is what says a new one begins. The first
   heading follows the page's own title and keeps the closer spacing. */
.panel + .section-head { margin-top: 40px; }
.section-title { font-size: 1rem; font-weight: 650; letter-spacing: -.01em; }
.section-head .spacer { margin-left: auto; }

/* ── 10. Statistics ──────────────────────────────────────────────────── */

.stats-row { display: flex; flex-wrap: wrap; }

/* the compact version that sits in a band above the list.

   nowrap, unlike the strips it inherits from: this band gains a counter the moment a
   filter is on, and wrapping would drop "All statistics" to a second row and deepen the
   whole panel — the page below it would step down every time a filter was touched. The
   gap is tight enough that Found arrives without anything else having to move; past that
   the chart gives up width, since it reads as a shape rather than by the pixel. */
.stats-strip { gap: 16px; flex-wrap: nowrap; }
/* The compact counter belongs to the band, not only to the strip class: a .strip-stats group
   carries the same reading inside a plain strip, and it has to read the same there. */
.stats-strip .stat, .strip-stats .stat {
  display: inline-flex; align-items: baseline; gap: 7px;
  flex: 0 0 auto; min-width: 0; padding: 0; border-right: 0; white-space: nowrap;
}
.stats-strip .stat-label, .strip-stats .stat-label { display: inline; margin-bottom: 0; }
.stats-strip .stat-value, .strip-stats .stat-value { font-size: .9375rem; }
/* A section label standing beside the counters is set in the same type as their labels, so
   it has to sit on their baseline — centred against the taller stat boxes it rides a couple
   of pixels low and the row of small caps reads as crooked. They are grouped in a box of
   their own rather than baseline-aligned in the strip itself: a strip that also carries
   buttons is taller than the readings, and a baseline group there would hang from the top
   of the line instead of sitting in the middle of the band. */
.strip-stats { display: flex; align-items: baseline; gap: 16px; min-width: 0; }
/* the filter's own count: a hairline off the totals, and the accent on the number */
.stats-strip .stat.is-found {
  padding-left: 22px; border-left: 1px solid var(--line); margin-left: 0;
}
.stats-strip .stat.is-found .stat-label,
.stats-strip .stat.is-found .stat-value { color: var(--accent); }
/* the one counter that is a way in rather than a reading: warm, and it answers the pointer.
   Not tied to the band: the statistics page carries the same counter as a full-height tile,
   and it has to read the same there. */
.stat.is-dupe .stat-label,
.stat.is-dupe .stat-value { color: var(--warn); }
a.stat:hover .stat-value { text-decoration: underline; }

.stat {
  flex: 1 1 0; min-width: 110px; padding: 14px 20px;
  border-right: 1px solid var(--line-soft);
}
.stat:last-child { border-right: 0; }
.stat-label {
  display: block; font-size: .65625rem; text-transform: uppercase;
  letter-spacing: .09em; font-weight: 700; color: var(--muted); margin-bottom: 5px;
}
.stat-value { font-size: 1.375rem; font-weight: 650; letter-spacing: -.02em; color: var(--ink); }

/* column chart: one series, one hue, 2px gaps, data ends rounded */
.chart-columns { display: flex; align-items: flex-end; gap: 2px; height: 132px; }
.column { position: relative; flex: 1 1 0; height: 100%; display: flex; align-items: flex-end; min-width: 3px; }
.column-fill {
  position: relative;
  width: 100%; background: var(--accent); border-radius: 4px 4px 0 0; min-height: 3px;
}
.column-fill.is-zero { background: var(--line); min-height: 2px; }
/* the count standing on top of its column — quiet enough to read the shape past it */
.column-value {
  position: absolute; bottom: calc(100% + 4px); left: 50%; transform: translateX(-50%);
  font-size: .625rem; font-weight: 700; line-height: 1; color: var(--muted);
  font-variant-numeric: tabular-nums; pointer-events: none;
}
.column:hover .column-value { color: var(--ink); }
.chart-axis {
  display: flex; align-items: center; gap: 8px; margin-top: 9px;
  font-size: .65625rem; text-transform: uppercase; letter-spacing: .09em;
  font-weight: 700; color: var(--muted);
}
.chart-axis .spacer { margin-left: auto; }
/* the reading appears the moment the pointer lands, instead of a second later:
   a drawn tooltip in the band reserved above the plot, not a title attribute */
.chart:not(.is-mini) { padding-top: 30px; }
.chart:not(.is-mini) .column:hover .column-fill { background: var(--accent-dark); }
.chart:not(.is-mini) .column:hover .column-fill.is-zero { background: var(--off-dot); }
.chart:not(.is-mini) .column::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + 5px); left: 50%; transform: translateX(-50%);
  z-index: 5; padding: 3px 9px; border-radius: 6px;
  background: var(--ink); color: var(--bg);
  font-size: .71875rem; font-weight: 600; letter-spacing: .01em;
  white-space: nowrap; opacity: 0; pointer-events: none;
}
.chart:not(.is-mini) .column:hover::after { opacity: 1; }
/* the columns at either end hang their label inward so the panel cannot clip it */
.chart:not(.is-mini) .column:nth-child(-n+3)::after { left: 0; transform: none; }
.chart:not(.is-mini) .column:nth-last-child(-n+3)::after { left: auto; right: 0; transform: none; }

/* the one thing in the strip that gives up width. The bars compress with it down to their
   3px floor; only a window narrower than the list was drawn for gets past that, and there
   the newest days clip off the right rather than the counters being pushed around. */
.chart.is-mini { flex: 0 1 auto; width: 150px; min-width: 0; overflow: hidden; }
.chart.is-mini .chart-columns { height: 24px; gap: 2px; }
.chart.is-mini .column-fill { border-radius: 2px 2px 0 0; }
/* There is width for one optional counter, not two. With both the duplicates count and a
   filter's Found out, giving up width is no longer enough — the bars go to a sliver and
   "All statistics" behind them breaks over three lines. So the chart leaves instead: it is
   the one thing in the strip that is whole one click away, and the counters are not. */
.stats-strip:has(.stat.is-dupe):has(.stat.is-found) .chart.is-mini { display: none; }
/* and with all three optional counters out at once, the gaps give up 4px each rather than
   the link at the end breaking over two lines — it is the last 16px, and no counter moves
   far enough for the row to read as any tighter */
.stats-strip:has(.stat.is-dupe):has(.stat.is-found):has(.stat.is-parsed) { gap: 12px; }

/* horizontal bars inside the sources table, and the export split */
.bar-track {
  display: block; height: 8px; background: var(--strip);
  border-radius: 6px; overflow: hidden;
}
/* a share of the whole account, so most bars are short — keep a sliver visible rather than
   letting a source that is still running read as nothing at all */
.bar-fill { display: block; height: 100%; min-width: 2px; background: var(--accent); border-radius: 6px; }
/* the bar takes the free width of the cell; the number keeps its own, so short bars stay
   readable and the column of percentages lines up */
.cell-pair .bar-track { flex: 1 1 auto; }
.share-value {
  flex: none; width: 48px; text-align: right;
  font-size: .8125rem; font-variant-numeric: tabular-nums; color: var(--body);
}
.split-bar {
  height: 12px; background: var(--off-dot); border-radius: 6px; overflow: hidden;
}
/* The share reads off the head of the bar rather than from a line under it: the number is
   what the panel is for, and the bar runs out from it, taking the free width. The gap is the
   field's own side padding, and the leading number gives up the fixed width the table's
   column of percentages needs — here there is one bar and nothing to line up with, so the
   box hugs the text and sits as far from the bar as from the panel edge. */
.split-row { display: flex; align-items: center; gap: 20px; }
.split-row .split-bar { flex: 1 1 auto; }
.share-value.is-leading { width: auto; text-align: left; }
.split-fill { height: 100%; background: var(--accent); border-radius: 6px 0 0 6px; }

/* The export page lays its bar out rather than filling it from the left: each state takes
   its own share of the width, in the colour the dot beside it carries. Rounded at the ends
   of the bar and square between, so the runs read as one bar divided and not as two bars set
   down in a row. A state with nothing in it is left out of the markup, which is what keeps
   the rounding on whichever states are actually there. */
.split-bar.is-stacked { display: flex; }
.split-bar.is-stacked .split-fill { border-radius: 0; min-width: 2px; }
.split-bar.is-stacked .split-fill:first-child { border-radius: 6px 0 0 6px; }
.split-bar.is-stacked .split-fill:last-child { border-radius: 0 6px 6px 0; }
.split-fill.is-pending { background: var(--warn); }

.empty { padding: 56px 20px; text-align: center; color: var(--body); font-size: .875rem; }
.empty .btn-primary { margin-top: 14px; }

.value { font-size: .875rem; color: var(--ink); overflow-wrap: anywhere; }
.value.is-mono { font-family: var(--font-mono); font-size: .8125rem; }

/* a message, not another band of chrome: tinted, with an accent edge of its own */
.flash {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: 16px; padding: 11px 16px;
  background: var(--accent-bg); border: 1px solid var(--accent-line);
  border-left: 3px solid var(--accent); border-radius: 10px;
  font-size: .84375rem; color: var(--ink);
}
.flash-mark {
  flex: none; font-size: .65625rem; text-transform: uppercase;
  letter-spacing: .09em; font-weight: 700; color: var(--accent);
}
.flash.is-error {
  background: var(--danger-bg); border-color: var(--danger-line);
  border-left-color: var(--danger); color: var(--danger);
}
.flash .spacer { margin-left: auto; }
.flash-close {
  flex: none; background: none; border: 0; padding: 0 2px; margin: 0;
  font-size: 1.125rem; line-height: 1; color: var(--muted);
  font-family: inherit; cursor: pointer;
}
.flash-close:hover { color: var(--ink); }
.flash.is-error .flash-close:hover { color: var(--danger); }
.flash.is-error .flash-mark { color: var(--danger); }

.dirty { font-size: .75rem; font-weight: 600; color: var(--warn); }

/* ── 11. Searchable select (Tom Select) ──────────────────────────────── */

/* Off the page but in the a11y tree: headings that carry no visible weight, and the
   original <select>, which tom-select replaces with markup of its own. */
.sr-only,
.ts-hidden-accessible {
  position: absolute !important; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.ts-wrapper { position: relative; width: 100%; }

.ts-control {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px;
  background: var(--surface); border: 1px solid var(--input-line);
  border-radius: 8px; padding: 9px 30px 9px 12px;
  font-size: .84375rem; color: var(--ink); cursor: text;
}
.ts-control > input {
  flex: 1 1 auto; min-width: 4ch; width: auto; max-width: 100%;
  margin: 0; padding: 0; border: 0; background: transparent;
  font: inherit; color: inherit; box-shadow: none; outline: none;
}
.ts-control > input:focus { border: 0; box-shadow: none; }
.ts-control > input::placeholder { color: var(--muted); }
/* single mode parks the caret off-screen once an item is picked */
.ts-wrapper.input-hidden .ts-control > input { opacity: 0; position: absolute; left: -10000px; }

/* picked sources, one chip each */
/* the remove button is absolute so the label itself can carry the ellipsis —
   sources are long and the cell is 180px wide */
.ts-wrapper.multi .ts-control > .item {
  position: relative; display: block; max-width: 100%;
  background: var(--strip); border: 1px solid var(--line);
  border-radius: 6px; padding: 1px 20px 1px 6px;
  font-size: .78125rem; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ts-wrapper.multi .ts-control > .item.active { border-color: var(--muted); }
.ts-wrapper.multi .ts-control > .item > .remove {
  position: absolute; top: 0; bottom: 0; right: 1px;
  display: flex; align-items: center; padding: 0 4px;
  color: var(--muted); font-size: .875rem;
  line-height: 1; text-decoration: none; cursor: pointer;
}
.ts-wrapper.multi .ts-control > .item > .remove:hover { color: var(--danger); }

/* the chevron, drawn rather than pulled in as an image */
.ts-wrapper::after {
  content: ""; position: absolute; right: 12px; top: 50%; pointer-events: none;
  width: 7px; height: 7px; margin-top: -5px; border-right: 1.5px solid var(--muted);
  border-bottom: 1.5px solid var(--muted); transform: rotate(45deg);
}
.ts-wrapper.dropdown-active::after { margin-top: -1px; transform: rotate(225deg); }

.ts-wrapper.focus .ts-control {
  border-color: rgba(15, 118, 110, .45); box-shadow: 0 0 0 3px rgba(15, 118, 110, .14);
}
.ts-wrapper.disabled .ts-control { background: var(--strip); color: var(--muted); cursor: default; }
.ts-wrapper.disabled::after { opacity: .5; }

/* dropdownParent is the body — the filter panel clips its own overflow */
.ts-dropdown {
  position: absolute; z-index: 20; margin-top: 4px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 8px; box-shadow: 0 6px 20px rgba(43, 41, 38, .12);
  font-size: .8125rem; overflow: hidden;
}
.ts-dropdown-content { max-height: 260px; overflow-y: auto; overscroll-behavior: contain; }
.ts-dropdown .option, .ts-dropdown .create, .ts-dropdown .no-results {
  padding: 7px 12px; color: var(--ink); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ts-dropdown .option.selected { font-weight: 600; }
.ts-dropdown .option.active, .ts-dropdown .create.active { background: var(--strip); }
.ts-dropdown .create { color: var(--body); }
.ts-dropdown .create strong { color: var(--ink); font-weight: 600; }
.ts-dropdown .no-results { color: var(--muted); cursor: default; }

/* in a band the control sizes to the row, like the plain inputs next to it */
.panel-strip .ts-wrapper { width: auto; min-width: 180px; max-width: 280px; }
.panel-strip .ts-control { padding: 6px 28px 6px 10px; font-size: .8125rem; }
