/* ===========================================================================
   Datadesk design system
   ---------------------------------------------------------------------------
   One stylesheet, no framework. Written for a dense internal research
   console, following the conventions of the data tools this most
   resembles: Linear's sidebar shell and quiet chrome, Stripe and Metabase
   for tabular density and numeric alignment, Sentry and Datadog for
   semantic status colour kept separate from the accent hue, GitHub for
   hairline rules and restrained hover states.

   Rules:
   - every colour comes from a token; component rules never name a colour
   - every size comes from the type or spacing scale
   - light and dark are the same structure with different token values

   The tokens themselves are in tokens.css, linked ahead of this file:
   they are shared with the other LNIC console, and a value defined in
   two places drifts.
   =========================================================================== */


/* Native controls, scrollbars and form widgets follow the scheme.
   `light dark` means "either, ask the OS", which is right only while the
   theme IS the OS's. The toggle stamps data-theme on :root and the tokens
   below follow it -- but color-scheme did not, so a person on a dark OS
   who chose Light got light tokens and dark browser chrome: unchecked
   checkboxes drawn as black boxes on a white card, and dark scrollbars.
   The stamp has to move both. */
html { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--accent);
  text-underline-offset: 2px;
}

a:hover { color: var(--accent-hover); }

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

code, .mono, .mono-inline {
  font-family: var(--font-mono);
  font-size: 0.92em;
  font-variant-ligatures: none;
}

.mono { white-space: pre-wrap; }

/* Motion here is functional and short. */
@media (prefers-reduced-motion: no-preference) {
  a, button, .nav-item, .facet, .band, .section-nav a,
  table.grid tbody tr, #results, #queue-results {
    transition: background-color 120ms ease, border-color 120ms ease,
                color 120ms ease, opacity 120ms ease;
  }
}

/* ===========================================================================
   Shell: persistent sidebar, thin top bar, content column
   =========================================================================== */

.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100vh;
}

.shell.no-sidebar { grid-template-columns: minmax(0, 1fr); }

.sidebar {
  background: var(--sidebar-bg);
  border-right: 1px solid var(--border);
  padding: var(--s-4) var(--s-3);
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

.brand a {
  display: block;
  padding: var(--s-1) var(--s-2);
  font-size: var(--fs-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
  text-decoration: none;
}

.nav-group + .nav-group { margin-top: var(--s-4); }

.nav-group-label {
  display: block;
  padding: 0 var(--s-2) var(--s-1);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-3);
}

.nav-item {
  display: block;
  padding: 0.3rem var(--s-2);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
  color: var(--text-2);
  text-decoration: none;
}

.nav-item:hover { background: var(--surface-3); color: var(--text); }

.nav-item.active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

/* A link to another LNIC console. It is a peer of the links above it,
   not a lesser thing, so it wears the same type and weight; the mark is
   the only difference, and it says "this navigates away" rather than
   "this is external to your work". */
.nav-item.nav-external {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-1);
}

.nav-away {
  color: var(--text-3);
  font-size: var(--fs-xs);
  line-height: 1;
}

.nav-item.nav-external:hover .nav-away,
.section-nav a.section-external:hover .nav-away { color: var(--text-2); }

.section-nav a.section-external .nav-away { margin-left: 0.35em; }

.sidebar-foot {
  margin-top: auto;
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.content-column {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.topbar {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-2) var(--s-5);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  min-height: 2.75rem;
}

.topbar .wordmark { margin-right: auto; font-weight: 600; }
.topbar .wordmark a { color: inherit; text-decoration: none; }

.identity {
  font-size: var(--fs-xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

.session { display: flex; align-items: center; gap: var(--s-3); }
.session form { margin: 0; }

main { flex: 1; padding: var(--s-5); min-width: 0; }

/* The menu button. Absent above 60rem, where the nav is a column that
   costs nothing to leave open. */
.nav-toggle { display: none; }
.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  display: block;
  width: 1rem;
  height: 2px;
  background: currentColor;
  border-radius: 1px;
}
.nav-toggle-bars { position: relative; }
.nav-toggle-bars::before,
.nav-toggle-bars::after { content: ""; position: absolute; left: 0; }
.nav-toggle-bars::before { top: -5px; }
.nav-toggle-bars::after { top: 5px; }

@media (max-width: 60rem) {
  .shell { grid-template-columns: minmax(0, 1fr); }
  .sidebar {
    position: static; height: auto;
    flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: var(--s-3);
  }
  .nav-group { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-1); }
  .nav-group + .nav-group { margin-top: 0; }
  .sidebar-foot { margin-top: 0; }
  main { padding: var(--s-4); }

  /* The brand and the button on one line, which is the whole sidebar
     while the menu is closed: about 3rem instead of 330px. */
  .brand {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    width: 100%;
  }
  .nav-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    min-height: 2.75rem;
    padding: var(--s-1) var(--s-3);
    font: inherit;
    font-size: var(--fs-sm);
    color: var(--text-2);
    background: none;
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    cursor: pointer;
  }
  .nav-toggle:hover { color: var(--text); background: var(--surface-2); }
  .nav-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .nav-toggle .nav-count {
    padding: 0 .35rem;
    border-radius: .6rem;
    background: var(--accent);
    color: var(--on-accent);
    font-size: var(--fs-2xs);
    font-variant-numeric: tabular-nums;
  }

  /* "Access by dataset" is orientation for the menu, so it keeps the
     menu's company. Derived from the button's own aria state rather than
     a second flag: that attribute has to be right for a screen reader
     anyway, and where `:has` is not understood the line simply stays,
     which is what it did before. It cannot move into the nav -- its
     `margin-top: auto` is what pins it to the foot of the column on a
     desktop. */
  .sidebar:has(.nav-toggle[aria-expanded="false"]) .sidebar-foot { display: none; }

  /* Open, the menu is the full list again -- a column, because a wrapped
     row of six groups is the thing being replaced. */
  .sidebar { flex-wrap: wrap; }
  .sidebar nav { flex-basis: 100%; }
  .sidebar nav .nav-group { display: block; }
  .sidebar nav .nav-group + .nav-group { margin-top: var(--s-3); }
}

/* ===========================================================================
   Cards and headings
   =========================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
  padding: var(--s-5);
  max-width: 46rem;
  margin-inline: auto;
}

.card + .card { margin-top: var(--s-4); }
.card-wide { max-width: none; }

h1 {
  margin: 0 0 var(--s-2);
  font-size: var(--fs-2xl);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: var(--lh-tight);
}

h2 {
  margin: 0 0 var(--s-3);
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Field-group labels in detail views: micro caps, never body text. */
h3 {
  margin: var(--s-5) 0 var(--s-2);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-3);
}

.lede {
  margin: 0 0 var(--s-4);
  max-width: 46rem;
  color: var(--text-2);
}

.breadcrumb { margin: 0 0 var(--s-2); font-size: var(--fs-xs); }
.breadcrumb a { color: var(--text-3); text-decoration: none; }
.breadcrumb a:hover { color: var(--text); }

.notice { color: var(--text-3); font-size: var(--fs-sm); }
.error { color: var(--tone-bad); }

.slug, .sub, .rationale { color: var(--text-3); font-size: var(--fs-xs); }
.sub, .rationale { display: block; margin-top: 0.1rem; line-height: var(--lh-tight); }
.absent { color: var(--text-3); }

/* ===========================================================================
   Buttons and controls
   =========================================================================== */

button {
  font-family: inherit;
  font-size: var(--fs-sm);
  padding: 0.3rem var(--s-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}

button:hover { background: var(--surface-2); }

button[disabled] {
  color: var(--text-3);
  background: var(--surface-2);
  border-color: var(--border);
  cursor: not-allowed;
}

button.primary, button.google-signin {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

button.google-signin { padding: var(--s-2) var(--s-5); font-size: var(--fs-md); }

button.linklike {
  border: none;
  background: none;
  padding: 0;
  color: var(--accent);
  font-size: var(--fs-xs);
  text-decoration: underline;
}

input, select, textarea {
  font-family: inherit;
  font-size: var(--fs-sm);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 0.25rem 0.4rem;
}

input::placeholder { color: var(--text-3); }

/* A hint is an aside about the field above it. Unstyled it was set in
   body text and spaced like a paragraph, which put a sentence the same
   weight as the record between one field and the next -- three pages
   styled it themselves and every other one read as prose. */
.hint {
  font-size: var(--fs-xs);
  color: var(--text-3);
  margin: var(--s-1) 0 0;
}
label.stacked + .hint { margin: calc(var(--s-1) * -1) 0 var(--s-3); }

/* Checkboxes and radios must not inherit the text-input box: padding on a
   checkbox inflates it, and an inflated checkbox sets the row height for
   the entire grid.

   The colours go back to the browser's own. A checkbox drawn with a text
   input's background and ink renders its frame in near-black, which is
   heavy beside the words it belongs to and reads as a filled box rather
   than an empty one. `accent-color` is the only colour worth setting: it
   is what the tick is drawn in. */
input[type="checkbox"], input[type="radio"] {
  padding: 0;
  border: none;
  margin: 0.2rem 0 0;
  background: revert;
  color: revert;
  accent-color: var(--accent);
}

/* ===========================================================================
   Filter bar
   =========================================================================== */

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-4);
  align-items: end;
  padding: var(--s-3);
  margin-bottom: var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.filter-bar label {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}

.filter-bar label.checkbox {
  flex-direction: row;
  align-items: center;
  gap: var(--s-1);
  text-transform: none;
  letter-spacing: 0;
}

.filter-bar input[type="number"] { width: 5rem; min-width: 0; }
.filter-bar select, .filter-bar input { min-width: 7rem; }

.export-link { font-size: var(--fs-xs); align-self: end; padding-bottom: 0.3rem; }

/* Active filters, as removable chips. */
.active-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  margin-bottom: var(--s-3);
}

.active-filters .caption {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.filter-chip {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-1);
  padding: 0.1rem var(--s-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--text-2);
  text-decoration: none;
  font-size: var(--fs-xs);
}

.filter-chip:hover { border-color: var(--tone-bad); color: var(--tone-bad); }
.filter-chip .k { color: var(--text-3); }
.filter-chip .v { font-weight: 600; color: var(--text); }
.filter-chip:hover .v { color: inherit; }
.filter-chip .x { font-size: var(--fs-2xs); opacity: 0.7; }

.clear-filters { font-size: var(--fs-xs); }

.result-count {
  margin: 0 0 var(--s-2);
  font-size: var(--fs-xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

.result-count strong { color: var(--text); font-weight: 600; }

/* ===========================================================================
   Tables — the core surface
   =========================================================================== */

table.grid {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--fs-sm);
}

table.grid th, table.grid td {
  padding: 0.35rem var(--s-3);
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border);
}

table.grid thead th {
  height: var(--row-h);
  padding-top: 0.3rem;
  padding-bottom: 0.3rem;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
  background: var(--surface-2);
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
}

table.grid tbody tr:hover { background: var(--surface-2); }
table.grid tbody tr:last-child td { border-bottom: none; }

/* Digits that sit in a column must line up. */
table.grid td.count, table.grid th.count, table.grid td.num,
.stat-value, .identity, td.nowrap { font-variant-numeric: tabular-nums; }

table.grid td.count, table.grid th.count { text-align: right; white-space: nowrap; }

td.nowrap { white-space: nowrap; color: var(--text-2); }

/* The data's own vocabulary — ids, FIPS, ZIPs, money — is monospaced. */
table.grid td.code, table.grid td.money {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
}

/* One line per row: a headline truncates with its full text in the title
   attribute rather than wrapping into a ragged three-line cell. */
.truncate {
  display: block;
  max-width: 30rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

table.grid td.col-title { min-width: 16rem; }

/* A column of chips or codes should not absorb the table's slack; the
   prose column should. */
table.grid th.shrink, table.grid td.shrink { width: 1%; white-space: nowrap; }
table.grid td.col-publisher {
  min-width: 10rem;
  color: var(--text-2);
  white-space: nowrap;
}

/* A wide grid scrolls inside its own viewport, and its header sticks. */
.table-viewport {
  max-height: calc(100vh - 20rem);
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.table-viewport thead th { position: sticky; top: 0; z-index: 2; }

/* Small tables inside a detail card: horizontal scroll only. */
.table-scroll { overflow-x: auto; }

.empty-row td { padding: var(--s-6) var(--s-3); text-align: center; }

.empty-state { display: block; color: var(--text-2); font-size: var(--fs-sm); }
.empty-state strong {
  display: block;
  margin-bottom: var(--s-1);
  color: var(--text);
  font-size: var(--fs-md);
}

/* Sortable headers. */
a.sort {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}

a.sort:hover, a.sort.active { color: var(--text); }
.sort-mark { color: var(--accent); }

nav.pager {
  display: flex;
  justify-content: space-between;
  gap: var(--s-3);
  margin-top: var(--s-3);
  font-size: var(--fs-sm);
}

nav.pager a { text-decoration: none; }

/* ===========================================================================
   Chips: status and other closed vocabularies
   ---------------------------------------------------------------------------
   Tone is chosen by data attribute, so a value the pipeline adds later
   still renders — in the neutral tone — rather than vanishing or
   borrowing a meaning it has not earned.
   =========================================================================== */
/* One row per dataset somebody holds, each its own form: applying a
   change to one must not carry the others with it. */
.grants{ min-width: 20rem; }
.grant-row{ display: flex; align-items: center; gap: .4rem;
  margin: .15rem 0; font-size: var(--fs-sm); }
.grant-scope{ flex: 1 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.grant-row select{ font: inherit; font-size: var(--fs-sm); }
/* The row that adds one sits apart from the rows that change one. */
.grant-add{ margin-top: .4rem; padding-top: .4rem;
  border-top: 1px dashed var(--border); }


.chip {
  display: inline-block;
  padding: 0.05rem 0.45rem;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  color: var(--tone-mute);
  background: var(--tone-mute-soft);
}

.chip[data-status="enriched"] { color: var(--tone-good); background: var(--tone-good-soft); }
.chip[data-status="enrichment_skipped"] { color: var(--tone-warn); background: var(--tone-warn-soft); }
.chip[data-status="labeled"] { color: var(--tone-info); background: var(--tone-info-soft); }
.chip[data-status="not_article"] { color: var(--tone-bad); background: var(--tone-bad-soft); }

/* The claim's basis: designated by the model, or assumed from the
   publication's own city. The difference matters to an operator. */
.chip[data-method="focus_model"] { color: var(--tone-good); background: var(--tone-good-soft); }
.chip[data-method="publication_place_assumed"] { color: var(--tone-warn); background: var(--tone-warn-soft); }

/* geo_skip_reason, the complete production vocabulary. Skipped by design
   reads differently from nothing to code, and both read differently from
   a gazetteer miss, which is a failure worth surfacing. */
.chip[data-skip="regional_uses_place_set"] { color: var(--tone-info); background: var(--tone-info-soft); }
.chip[data-skip="no_codeable_geography"] { color: var(--tone-mute); background: var(--tone-mute-soft); }
.chip[data-skip="not_scoped"] { color: var(--tone-mute); background: var(--tone-mute-soft); }
.chip[data-skip="publication_city_not_in_census_gazetteer"] { color: var(--tone-bad); background: var(--tone-bad-soft); }

.chip.plain {
  color: var(--text-2);
  background: none;
  border-color: var(--border);
  font-weight: 400;
}

/* ===========================================================================
   Confidence: a number and its magnitude
   =========================================================================== */

.conf {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-2);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
}

.conf-bar {
  position: relative;
  width: 2.5rem;
  height: 0.3rem;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  overflow: hidden;
  flex: none;
}

.conf-bar::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(var(--v, 0) * 100%);
  background: var(--tone-good);
  border-radius: inherit;
}

.conf[data-band="mid"] .conf-bar::after { background: var(--tone-warn); }
.conf[data-band="low"] .conf-bar::after { background: var(--tone-bad); }

.conf-value {
  min-width: 2.2rem;
  text-align: right;
  color: var(--text-2);
  font-family: var(--font-mono);
}

/* ===========================================================================
   Stat rows
   =========================================================================== */

.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--s-4);
  margin: 0 0 var(--s-5);
}

.stat {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.stat-label {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.stat-value {
  font-size: var(--fs-3xl);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.02em;
}

.stat-value a { color: inherit; text-decoration: none; }
.stat-value a:hover { color: var(--accent); }

.stat-detail { font-size: var(--fs-xs); color: var(--text-3); }

/* ===========================================================================
   Detail view: label/value pairs
   =========================================================================== */

.fact-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, max-content));
  gap: var(--s-3) var(--s-5);
  margin: 0 0 var(--s-4);
  padding: var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.fact-row dt {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.fact-row dd {
  margin: 0.1rem 0 0;
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}

.fact-row dd.mono-inline { font-family: var(--font-mono); }

.stored-text {
  font-size: var(--fs-sm);
  line-height: 1.6;
  max-height: 22rem;
  overflow-y: auto;
  padding: var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.stored-text p { margin: 0 0 var(--s-3); }

.entity-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--s-5);
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
  align-items: start;
}

.fips-list { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0; }

.fips-list .mono-inline {
  padding: 0.05rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-2);
}

.rationales dt {
  margin-top: var(--s-2);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.rationales dd { margin: 0; font-size: var(--fs-xs); color: var(--text-2); }

.edit-link { font-size: var(--fs-xs); margin-left: var(--s-2); font-weight: 400; }

.identity-panel {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--s-1) var(--s-4);
  margin: 0;
  font-size: var(--fs-sm);
}

.identity-panel dt {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
  align-self: baseline;
}

.identity-panel dd { margin: 0; }

/* ===========================================================================
   Facets and section navigation
   =========================================================================== */

.case-facets {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s-3);
  margin: 0 0 var(--s-4);
}

.facet {
  display: block;
  padding: var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: inherit;
  text-decoration: none;
}

.facet:hover { border-color: var(--border-strong); background: var(--surface-3); }
.facet.selected { border-color: var(--accent); background: var(--accent-soft); }

.facet-count {
  display: block;
  font-size: var(--fs-3xl);
  font-weight: 600;
  line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.facet-label { display: block; font-size: var(--fs-sm); font-weight: 600; }

.facet-note {
  display: block;
  margin-top: var(--s-1);
  font-size: var(--fs-xs);
  color: var(--text-3);
  line-height: var(--lh-tight);
}

.band-facets {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-1);
  margin: 0 0 var(--s-3);
}

.band-caption {
  margin-right: var(--s-2);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.band {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-1);
  padding: 0.1rem var(--s-2);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--text-2);
  text-decoration: none;
  font-size: var(--fs-xs);
}

.band:hover { border-color: var(--border-strong); background: var(--surface-2); }
.band.selected { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }

.band-count { font-weight: 600; font-variant-numeric: tabular-nums; }
.band-label { color: var(--text-3); }
.band.selected .band-label { color: inherit; }

.section-nav {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}

.section-nav a {
  display: block;
  padding: var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  text-decoration: none;
  color: inherit;
}

.section-nav a:hover { border-color: var(--border-strong); background: var(--surface-2); }

.section-title { display: block; font-size: var(--fs-md); font-weight: 600; }

.section-note {
  display: block;
  margin-top: 0.1rem;
  font-size: var(--fs-xs);
  color: var(--text-3);
  line-height: var(--lh-tight);
}

/* ===========================================================================
   Action bars and the disposition placeholder
   =========================================================================== */

.action-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  align-items: end;
  padding: var(--s-3);
  margin-bottom: var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.action-bar label {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}

.disposition-placeholder {
  padding: var(--s-3);
  margin: 0 0 var(--s-3);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-md);
  font-size: var(--fs-xs);
  color: var(--text-2);
}

.disposition-placeholder p { margin: 0 0 var(--s-2); }

.button-row { display: flex; flex-wrap: wrap; gap: var(--s-2); margin: 0; }

/* ===========================================================================
   Forms
   =========================================================================== */

label.stacked {
  display: block;
  margin: var(--s-3) 0;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}

label.stacked input, label.stacked textarea, label.stacked select {
  display: block;
  width: 100%;
  margin-top: var(--s-1);
  font-size: var(--fs-md);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  padding: 0.35rem var(--s-2);
}

.repair-preview {
  padding: var(--s-4);
  margin-bottom: var(--s-4);
  border: 1px solid var(--accent);
  border-radius: var(--r-md);
  background: var(--accent-soft);
}

.repair-diff dt {
  margin-top: var(--s-2);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.repair-diff dd { margin: 0; }

.columns-pick {
  padding: var(--s-3) var(--s-4);
  margin: var(--s-4) 0;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.columns-pick legend {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.columns-pick label {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  margin-right: var(--s-5);
  font-size: var(--fs-sm);
}

/* ===========================================================================
   Loading state
   ---------------------------------------------------------------------------
   Requests name their results container as hx-indicator, so htmx marks it
   .htmx-request while a fetch is in flight. Quiet, but present: on a
   corpus this size the wait is long enough to read as a dead click.
   =========================================================================== */

#results.htmx-request, #queue-results.htmx-request { opacity: 0.45; cursor: progress; }

#results.htmx-request::before, #queue-results.htmx-request::before {
  content: "Loading…";
  display: block;
  padding: var(--s-1) 0;
  font-size: var(--fs-xs);
  color: var(--text-3);
}

/* ===========================================================================
   Chart runtime and the visual builder
   =========================================================================== */

.dd-visual { margin: 0; }
.dd-subtitle { margin: 0 0 var(--s-3); font-size: var(--fs-md); color: var(--text-2); }
.dd-chart { min-height: 200px; }
.dd-chart svg { max-width: 100%; height: auto; display: block; margin: 0 auto; }

.dd-footer {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: var(--s-2);
}

.dd-source { font-size: var(--fs-xs); color: var(--text-3); }
/* Under the name, not beside it: the two say different things and a
   reader scanning for one should not have to read past the other. */
.dd-version { display: block; font-variant-numeric: tabular-nums; }

.dd-toggle {
  border: none;
  background: none;
  padding: 0;
  font-size: var(--fs-xs);
  color: var(--text-3);
  text-decoration: underline;
  cursor: pointer;
}

.dd-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-4);
  margin-bottom: var(--s-2);
  font-size: var(--fs-xs);
}

.dd-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  margin-right: 0.35rem;
}

.dd-swatch.round { border-radius: 50%; }

.dd-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }

.dd-table th, .dd-table td {
  padding: 0.3rem var(--s-2);
  text-align: left;
  border-bottom: 1px solid var(--border);
}

.dd-table td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Prose -- a paragraph quoted from a story -- at a width it can be read at.
   The table scrolls sideways before a paragraph is squeezed to a column of
   three words a line. */
.dd-table td.prose, .dd-table th.prose { min-width: 32rem; max-width: 48rem; }
.dd-table td.prose { line-height: 1.45; }
/* A link in a table looks like one: the accent blue, underlined faintly so
   a row of them does not read as a row of underlines, fully on hover. */
.dd-table td a { color: var(--accent); text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent) 35%, transparent);
  text-decoration-thickness: 1px; text-underline-offset: 3px; }
.dd-table td a:hover { text-decoration-color: currentColor; }

/* One row of controls above the table: the filter, and how many rows it
   left. Every column header is the sort control for its column. */
.dd-table-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2);
  margin: 0 0 var(--s-2); }
.dd-table-bar input[type="search"] { flex: 0 1 18rem; min-width: 0; }
.dd-table-count { color: var(--text-3); font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums; }
.dd-table-scroll { overflow-x: auto; max-width: 100%; }
.dd-table th { position: sticky; top: 0; background: var(--surface-2); padding: 0; }
.dd-table th button { width: 100%; border: 0; background: none; font: inherit;
  font-weight: 600; color: var(--text-2); text-align: left; cursor: pointer;
  padding: 0.3rem var(--s-2); white-space: nowrap; }
.dd-table th button:hover { color: var(--accent); background: var(--surface-3); }
.dd-table th.num button { text-align: right; }
.dd-table th[aria-sort] button { color: var(--text); }

/* A grouped table is one row per outermost group, and every column after
   it a stack of lines, one per record. The lines do not wrap: a wrapped line
   in one column would push every line below it out of step with its
   neighbours, and the row stops reading across. The table scrolls instead.

   An inner group is named on its first line. The repeats keep their text --
   for a screen reader, and for whoever copies the table out -- and are
   painted out. */
.dd-table tr.dd-grouped td { vertical-align: top; }

/* Under the rows, where somebody arrives after reading a page of them. */
.dd-pager { display: flex; align-items: center; gap: var(--s-2);
  margin: var(--s-2) 0 0; }
.dd-pager-at { color: var(--text-3); font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums; }
.dd-stack { display: flex; flex-direction: column; }
.dd-line { white-space: nowrap; line-height: 1.5; min-height: 1.5em; }
.dd-table .dd-said { color: transparent; }

.dd-tip {
  position: absolute;
  z-index: 20;
  pointer-events: none;
  padding: var(--s-2);
  max-width: 20rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-2);
  font-size: var(--fs-xs);
  line-height: 1.45;
  display: grid;
  grid-template-columns: auto auto;
  gap: 0.1rem var(--s-3);
}

.dd-tip strong { grid-column: 1 / -1; font-size: var(--fs-sm); }
.dd-tip-k { color: var(--text-3); }
.dd-tip-v { text-align: right; font-variant-numeric: tabular-nums; }
.dd-tip[hidden] { display: none; }
.dd-tip.pinned { pointer-events: auto; border-color: var(--accent); }

.dd-ramp { display: inline-flex; align-items: center; gap: var(--s-2); color: var(--text-3); }
.dd-ramp span { display: inline-flex; align-items: center; gap: var(--s-1); }

/* THE SCALE IS ONE OBJECT, so it is drawn as one strip rather than as a
   labelled chip per band. Eleven chips reading "143-208" across the top
   of a map do not fit and wrap into a paragraph of numbers; butted
   swatches with a few boundaries written underneath is how a reader uses
   a choropleth key anyway -- to place a shade between two ends. */
/* A DEFINITE WIDTH, not a flex basis. The blocks are `flex: 1 1 0` with
   `min-width: 0`, so their intrinsic width is zero; inside a
   content-sized `inline-flex` parent that collapsed the whole bar to
   0px -- no colour at all -- and the milestones, positioned as
   percentages of a zero-width box, all landed on the same point and
   stacked on top of each other. */
.dd-ramp-strip { display: inline-flex !important; flex-direction: column;
  gap: 3px !important; align-items: stretch;
  width: 13rem; flex: 0 0 auto; }
/* Flush, with the rounding on the bar rather than on each block: the eye
   reads one object. A ramp with slots cut into it says "categories",
   which is the opposite of what a sequential scale means. */
.dd-ramp-blocks { display: flex !important; gap: 0 !important;
  width: 100%; height: 10px; border-radius: 5px; overflow: hidden; }
.dd-ramp-block { display: block !important; flex: 1 1 0; min-width: 0;
  height: 100%; }
/* The milestones sit where their value falls, so the row is a
   positioning context rather than a flex line -- a flex row would space
   them evenly and put every number somewhere it does not belong. */
.dd-ramp-marks { display: block !important; position: relative;
  width: 100%; height: .8rem;
  font-size: .64rem; line-height: 1; font-variant-numeric: tabular-nums;
  color: var(--text-3); }
.dd-ramp-mark { position: absolute; top: 0; transform: translateX(-50%);
  white-space: nowrap; }
/* The two ends would otherwise hang off the bar. */
.dd-ramp-mark[data-align="start"] { transform: none; }
.dd-ramp-mark[data-align="end"] { transform: translateX(-100%); }
@media (max-width: 34rem) { .dd-ramp-strip { width: 10rem; } }

.dd-beyond { color: var(--text-3); font-style: italic; }

.builder {
  display: grid;
  grid-template-columns: 22rem minmax(0, 1fr);
  gap: var(--s-5);
  align-items: start;
}

@media (max-width: 70rem) {
  .builder { grid-template-columns: minmax(0, 1fr); }
}

.builder-form label {
  display: block;
  margin: var(--s-2) 0;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}

.builder-form select, .builder-form input[type="text"] {
  display: block;
  width: 100%;
  margin-top: var(--s-1);
  font-size: var(--fs-sm);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
}

.builder-form .inline { display: flex; align-items: center; gap: var(--s-1); }

/* The walk's step form is #step-form, not .builder-form, so it never
   picked either of these up: two checkbox labels in a row ran together on
   one line, and a note under a control was set in body text. */
/* The schema page: a field's name, the key it is stored under, and the
   words a vocabulary holds. Each row is three facts about one thing, and
   without these they ran together into "Host host". */
.schema .field-name { display: block; font-weight: 600; }
.schema .field-key,
.schema .terms code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-xs);
  color: var(--text-3);
}
/* A publisher on the paywalls page: the name opens the record, the host
   opens the publication. The host is a web address rather than a title,
   so it is not set like one. */
/* The name of a field, then whether the record needs it. One line of
   label text ran them together into "Owner worth having". */
label.stacked .field-label { margin-right: var(--s-2); }
/* A status, not another word in the name. The label is already small,
   uppercase and grey, so a marker in the same size and colour read as
   part of it -- "Publication name required" as one phrase. This is
   smaller, lighter in weight and tinted, and sits in its own pill. */
label.stacked .required {
  display: inline-block;
  padding: 0.02rem 0.35rem;
  border-radius: var(--r-pill);
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--tone-warn);
  background: var(--tone-warn-soft);
  vertical-align: 0.08em;
}

/* How far a paywalled publisher's sign-in has got. Three states,
   because credentials alone are not enough: bought, configured, and
   proved to read. */
.stage {
  display: inline-block;
  padding: 0.05rem var(--s-2);
  border-radius: var(--r-pill);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.stage-verified { color: var(--tone-good); background: var(--tone-good-soft); }
.stage-configured { color: var(--tone-warn); background: var(--tone-warn-soft); }
.stage-credentialed { color: var(--tone-info); background: var(--tone-info-soft); }

.publisher .goes {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  margin: 0 0 var(--s-1);
}
/* The owner's label, wherever it appears: the paywalls page's publisher
   column and the extraction queue's publisher header. Said once so the
   two cannot drift into two different-looking answers to the same
   question. */
.publisher .goes-to,
.rec-head .goes-to {
  font-size: var(--fs-2xs);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}
/* Only the paywalls column reserves a gutter, so its labels line up down
   the column. In the queue header the label sits inline after the name. */
.publisher .goes-to { flex: 0 0 3.4rem; }
/* The owner is a fact, not a destination: same row shape as the links
   above it so the labels line up, but not styled as one. */
.publisher .owner,
.rec-head .owner { font-size: var(--fs-sm); color: var(--text-2); }
.publisher .no-paywall { margin-top: var(--s-1); font-size: var(--fs-sm); }

/* The paywall row's form. Labels sit beside their fields rather than
   above: stacked, three fields and a button ran the column deeper than
   the row needed and pushed the table past a laptop's width. */
label.side { display: grid; grid-template-columns: 7.5rem 1fr; align-items: center;
  gap: 0.125rem; margin-bottom: var(--s-1); font-size: var(--fs-sm);
  color: var(--text-2); }
label.side input, label.side select { width: 100%; min-width: 0; }
.last-comment { margin: 0 0 var(--s-2) calc(7.5rem + 0.125rem); font-style: italic; }

/* Save sits under the comment box and hard right, where the eye already
   is after typing into the last field. */
.inline-form .form-actions { display: flex; justify-content: flex-end;
  align-items: center; gap: var(--s-2); }

/* A row with changes not yet saved. The page previously looked the same
   whether somebody had typed or not, so a half-filled row and a saved
   one were indistinguishable -- and the hint underneath said "not
   recorded yet" either way. */
.unsaved-note { display: none; margin-left: var(--s-2); font-size: var(--fs-sm);
  font-weight: 600; color: var(--tone-warn); }
.inline-form.dirty .unsaved-note { display: inline; }
.inline-form.dirty { outline: 2px solid var(--tone-warn); outline-offset: .35rem;
  border-radius: .25rem; }
.inline-form.dirty button[type="submit"] { font-weight: 700; }
/* The line that says whether the row is recorded sits under Save and
   ends where the button ends, so the column has one right edge rather
   than a button on one side and its status on the other. */
.saved-state { text-align: right; }
.saved-state.not-saved { color: var(--text-2); }
tr:has(.inline-form.dirty) .saved-state { visibility: hidden; }
.publisher .site {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-xs);
  font-weight: 400;
}

/* The record, opened from the queue card that asks about it. A dialog
   rather than a page, so a reviewer answering four questions about one
   publisher does not lose the queue to edit it. */
.rec-editor {
  width: min(46rem, 92vw);
  max-height: 88vh;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 0;
  background: var(--surface);
  color: var(--text);
}
.rec-editor::backdrop { background: rgb(0 0 0 / 0.45); }
.rec-editor .card { box-shadow: none; margin: 0; }
.rec-editor-close { float: right; margin: var(--s-2) var(--s-2) 0 0; }
.rec-head h2 a { color: inherit; }
/* Whole-document, for the same form served without the console around
   it. The dialog inserts it as markup, so this is what styles it. */
body.bare { margin: 0; background: transparent; }

/* The kinds a field can hold, and under each the words that mean it.
   Listed as words instead, a row read "digital counts as Digital written
   digital native" -- three values with nothing saying which was which. */
/* How much a record needs a field. The middle one is the one that was
   missing: asked for, and empty is an answer. */
.schema .need .hint { display: block; }
.schema .need-optional { color: var(--text-3); }

.schema .kinds { margin: var(--s-2) 0; padding: 0; list-style: none; }
.schema .kinds > li {
  margin: 0 0 var(--s-3);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--border);
}
.schema .kinds > li:last-child { border-bottom: 0; }
.schema .kind-head { margin: 0 0 var(--s-1); }
.schema .kind-head .hint { margin-left: var(--s-2); }
.schema .chips { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: 0; }
/* `word`, not `chip`. The chip component draws its own "×" with a
   ::after and turns solid on hover, which put two crosses on every one
   of these -- one of them a button and the other decoration. */
.schema .word {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 0.1rem var(--s-2);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}
.schema .word.retired code { text-decoration: line-through; color: var(--text-3); }
.schema .word button { padding: 0; line-height: 1; }
.schema .add-word { display: flex; gap: var(--s-1); margin-top: var(--s-2); }
.schema .add-word input { flex: 1 1 12rem; }
.schema .new-kind { margin-top: var(--s-2); }
.schema .inline-form { display: inline; }
.schema .add-term { margin-top: var(--s-3); }
.schema .hint { font-size: var(--fs-xs); color: var(--text-3); margin: var(--s-1) 0; }

#step-form .inline {
  display: flex;
  align-items: baseline;
  gap: var(--s-1);
  margin: var(--s-2) 0;
}

#step-form p.hint {
  margin: calc(var(--s-1) * -1) 0 var(--s-3);
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.builder-form fieldset {
  margin: var(--s-4) 0;
  padding: var(--s-2) var(--s-3) var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.builder-form legend {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.builder-preview { position: sticky; top: var(--s-4); }

.data-step {
  padding: var(--s-2) var(--s-4) var(--s-4);
  margin-bottom: var(--s-5);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.data-step > summary { cursor: pointer; font-size: var(--fs-sm); color: var(--text-2); }

.pivot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--s-2) var(--s-4);
  margin: var(--s-3) 0;
}

.pivot-grid label {
  display: block;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}

.pivot-grid select, .pivot-grid input {
  display: block;
  width: 100%;
  margin-top: var(--s-1);
  font-size: var(--fs-sm);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
}

.pivot-grid label.inline { display: flex; align-items: center; gap: var(--s-1); }
.pivot-grid label.inline input { width: auto; margin: 0; }

.grid-bar {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin: var(--s-2) 0 var(--s-1);
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.grid-scroll {
  max-height: 22rem;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}

table.data-grid { font-size: var(--fs-xs); }

table.data-grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: var(--s-1) 0.4rem;
  background: var(--surface-2);
  vertical-align: top;
}

table.data-grid tr.grid-filters th { top: 2.6rem; }
table.data-grid tr.grid-filters input { width: 100%; padding: 0.15rem 0.3rem; font-size: var(--fs-2xs); }

.grid-sort {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.grid-sort em {
  font-style: normal;
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-3);
}

.grid-sort i { font-style: normal; }
.grid-stat { display: block; font-weight: 400; font-size: var(--fs-2xs); color: var(--text-3); }
table.data-grid td.unselected { opacity: 0.3; }
.kind-hint.warn { color: var(--tone-bad); }

/* A capture long enough to be a real article is the point of the queue. */
td.count.flagged-length { color: var(--tone-warn); font-weight: 600; }

/* Wire: a syndicated story is an editorial fact, an unfinished check is a
   gap, and a local story is the ordinary case and stays quiet. */
.chip[data-wire="wire"] { color: var(--tone-warn); background: var(--tone-warn-soft); }
.chip[data-wire="incomplete"] { color: var(--tone-mute); background: var(--tone-mute-soft); }
.chip[data-wire="local"] { color: var(--text-3); background: none; border-color: var(--border); font-weight: 400; }

.callout {
  padding: var(--s-3);
  margin: 0 0 var(--s-4);
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  color: var(--text-2);
}

.callout strong { color: var(--text); }
.callout-wire { border-left-color: var(--tone-warn); background: var(--tone-warn-soft); }

.geography {
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}

/* ===========================================================================
   Detail panels
   ---------------------------------------------------------------------------
   A fixed two-column shell leaves a hole whenever one side is shorter
   than the other. These panels flow into whatever columns the viewport
   affords, densely, so a short article and a long one both fill the page:
   one column on a narrow viewport, two from 68rem up. Tables carrying
   five columns span the full row; compact label/value panels sit two-up,
   and dense flow pulls a short panel up into any gap a taller neighbour
   would otherwise leave. A third column was tried and dropped: at three
   tracks a full-row table and a two-track table cannot both look
   deliberate.
   =========================================================================== */

.panels {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-auto-flow: dense;
  gap: var(--s-4);
  align-items: start;
}

@media (min-width: 68rem) {
  .panels { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .panel.wide { grid-column: 1 / -1; }
}

.panel {
  min-width: 0;
  padding: var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
}

.panel > h2 {
  margin: 0 0 var(--s-3);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: 0;
}

.panel > h2 + .notice { margin-top: 0; }
.panel > :last-child { margin-bottom: 0; }

/* The text panel spans the width; the prose inside keeps a reading
   measure rather than running the length of a 1600px viewport. */
.panel-text { margin-bottom: var(--s-4); }
.panel-text .stored-text p { max-width: 74ch; }

/* Inside a panel the label/value list is already framed, so it drops its
   own box and just lays out. */
.fact-row.plain {
  padding: 0;
  border: none;
  background: none;
  margin-bottom: 0;
}

/* --- roles and messages --- */
.chip[data-role="admin"] { color: var(--tone-bad); background: var(--tone-bad-soft); }
.chip[data-role="editor"] { color: var(--tone-info); background: var(--tone-info-soft); }
.chip[data-role="viewer"] { color: var(--tone-mute); background: var(--tone-mute-soft); }

.role-form { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; margin: 0; }
.role-form input[type="text"] { min-width: 12rem; }

.messages { list-style: none; margin: 0 auto var(--s-4); padding: 0; max-width: 46rem; }
.card-wide ~ .messages, .messages { max-width: none; }
.message {
  padding: var(--s-2) var(--s-3);
  margin-bottom: var(--s-2);
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--r-md);
  background: var(--surface);
  font-size: var(--fs-sm);
}
.message.success { border-left-color: var(--tone-good); }
.message.error { border-left-color: var(--tone-bad); color: var(--tone-bad); }
.message.warning { border-left-color: var(--tone-warn); }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.dd-title { font-size: 1.15rem; margin: 0 0 0.25rem; }

/* --- proposal queue --- */
.queue { padding-bottom: 5rem; }
.queue-head h1 { margin-bottom: .2rem; }
.queue-facets { display: flex; flex-wrap: wrap; gap: .4rem; margin: 1rem 0 1.25rem; }
.facet-chip { display: inline-flex; align-items: center; gap: .4rem; padding: .28rem .6rem;
  border: 1px solid var(--border); border-radius: 999px; font-size: .82rem;
  text-decoration: none; color: inherit; }
.facet-chip span { color: var(--text-2); font-variant-numeric: tabular-nums; }
.facet-chip.on { border-color: currentColor; font-weight: 600; }
/* A directory with nothing left in it. Still selectable -- the decided
   proposals are still there to look at -- but it should not compete for
   attention with the ones holding work. */
.facet-chip.done { opacity: .62; }
.facet-chip.done span { font-variant-numeric: normal; font-style: italic; }
.facet-chip.done.on { opacity: 1; }

/* A row whose question has already been answered. It carries the answer
   instead of the buttons: shown with live controls, "Including decided"
   put 316 settled questions back on the page looking unsettled. */
.decided-outcome { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
.decided-verb { font-weight: 600; font-size: .82rem; letter-spacing: .02em;
  padding: .1rem .45rem; border-radius: .25rem; border: 1px solid currentColor; }
.decided-verb.accepted, .decided-verb.fixed { color: var(--tone-good); }
.decided-verb.rejected { color: var(--text-2); }
/* The extraction queue's verbs are the present tense of the same three. */
.decided-verb.accept, .decided-verb.reextract { color: var(--tone-good); }
.decided-verb.reject { color: var(--text-2); }
.decided-when { color: var(--text-2); font-size: .82rem; }

/* An article held for review that cannot be released. Not a styling
   flourish: it is the only place this failure is visible, because the
   status it was held from is gone and no verb can put it back. */
.hint.stranded { color: var(--tone-warn); font-weight: 600; }

/* What is waiting for the person reading, above the corpus figures: they
   say how the collection is doing, this says what somebody is expected to
   do about it. */
.todo { margin: 0 0 var(--s-4) 0; padding: var(--s-3); border: 1px solid var(--border);
  border-radius: .4rem; background: var(--accent-soft); }
.todo h2 { margin: 0 0 var(--s-2); font-size: var(--fs-md); }
.todo-dataset { display: flex; gap: var(--s-3); align-items: baseline;
  padding: var(--s-1) 0; flex-wrap: wrap; }
.todo-name { min-width: 12rem; font-weight: 600; }
.todo-tasks { list-style: none; margin: 0; padding: 0; display: flex;
  gap: var(--s-3); flex-wrap: wrap; }
.todo-tasks li { display: flex; gap: var(--s-1); align-items: baseline; }
.todo-tasks b { font-variant-numeric: tabular-nums; }
.todo-note { color: var(--text-2); font-size: var(--fs-sm); }
.todo-clear { margin-bottom: var(--s-4); }
.todo-age { color: var(--text-3); font-size: var(--fs-2xs); margin-left: auto; }

/* The same count in the sidebar, so it is visible from every page rather
   than only the one somebody lands on. */
.nav-item .nav-count { margin-left: .4rem; padding: 0 .35rem; border-radius: .6rem;
  background: var(--accent); color: var(--on-accent); font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums; }
.rec { border: 1px solid var(--border); border-radius: 6px; margin-bottom: 1rem; overflow: hidden; }
.rec-head { display: flex; align-items: baseline; gap: .75rem; padding: .6rem .9rem;
  border-bottom: 1px solid var(--border); }
.rec-head h2 { font-size: .95rem; margin: 0; }
.rec-src { margin-left: auto; font-size: .75rem; color: var(--text-2); }
table.rec-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
table.rec-table th, table.rec-table td { padding: .5rem .7rem; text-align: left;
  vertical-align: top; border-bottom: 1px solid var(--border, #eee); font-weight: 400; }
table.rec-table thead th { font-size: .7rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-2); }
table.rec-table tbody th { white-space: nowrap; color: var(--text-2); }
.rec-table .val { font-family: ui-monospace, monospace; font-size: .8rem; }
.rec-table .proposed { font-weight: 600; }
.rec-table .none { color: var(--text-2); font-style: italic; font-family: inherit; }
.rec-table tr.decided { background: rgba(127,127,127,.06); }
/* One decision per article rather than per field, so the verbs sit under
   the evidence in a bar of their own instead of in a column beside it. */
.rec-facts tbody th { width: 8.5rem; }
.rec-facts tbody tr:last-child th, .rec-facts tbody tr:last-child td { border-bottom: 0; }
.rec-facts .sub { display: block; font-size: .75rem; color: var(--text-2); }
.rec-facts .finding { display: inline; margin-left: .4rem; }
.rec-facts .flagged-length .val { font-weight: 600; }
/* "Actually it is" is a different question from the verbs, and sat flush
   against Reject as though it were part of it. Separated, labelled, and
   pushed to its own end of the row. */
/* Its own line, always. It wrapped onto one at this column width anyway,
   and a select sitting flush under Reject reads as part of Reject --
   which is the opposite of what it is: a different question, answered
   alongside the verb rather than instead of it. */
/* What the marked decision will do, said before it is submitted. */
.rec-decide .outcome-line { flex-basis: 100%; margin: .5rem 0 0; font-size: .8rem;
  color: var(--text-2); line-height: 1.4; }
.rec-decide .outcome-line:empty { margin: 0; }

.qualifier { display: flex; align-items: center; gap: .5rem;
  flex-basis: 100%; margin-top: .6rem; padding-top: .6rem;
  border-top: 1px solid var(--border); }
.qualifier > span { font-size: .72rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-2); white-space: nowrap; }
.qualifier .qualval { font-size: .82rem; padding: .25rem .4rem; max-width: 16rem; }

.rec-decide { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  padding: .6rem .9rem; border-top: 1px solid var(--border);
  background: rgba(127,127,127,.04); }
.rec-decide.decided { background: rgba(127,127,127,.09); }
.rec-decide.decided-outcome { font-size: .82rem; color: var(--text-2); }
.rec .notice.bad { margin: .6rem .9rem; }

.verbs { display: flex; gap: .3rem; flex-wrap: wrap; }
.verb { display: flex; flex-direction: column; align-items: flex-start; gap: .05rem;
  line-height: 1.2; min-width: 5.6rem; text-align: left; cursor: pointer;
  border: 1px solid var(--border); border-radius: 3px; background: none;
  padding: .25rem .5rem; color: inherit; font: inherit; }
.verb b { font-size: .8rem; font-weight: 600; }
.verb i { font-style: normal; font-size: .68rem; color: var(--text-2); }
.verb[aria-pressed="true"] { border-color: currentColor; }
.verb.accept[aria-pressed="true"] { color: #1c7049; }
.verb.reject[aria-pressed="true"] { color: #4a5058; }
.verb.fix[aria-pressed="true"] { color: #8a5a12; }
.decide .fixval { display: block; width: 100%; margin-top: .35rem; padding: .25rem .4rem;
  font-size: .82rem; box-sizing: border-box; }
.finding { margin: .35rem 0 0; font-size: .78rem; color: var(--text-2); }
.finding.owner_conflict, .finding.gazetteer { color: #a8452c; }
.sugg { margin: .1rem 0 0; font-size: .78rem; color: var(--accent, #2f5d8a); }
.blocked { padding: .6rem .9rem; background: rgba(127,127,127,.05); }
.blocked h3 { font-size: .75rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-2); margin: 0 0 .3rem; }
.blocked ul { margin: 0; padding-left: 1.1rem; font-size: .82rem; color: var(--text-2); }
.queue-dock { position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; align-items: center; gap: 1rem; padding: .7rem 1.25rem;
  background: var(--surface); border-top: 1px solid var(--border); }
.queue-dock .tally { font-size: .85rem; color: var(--text-2); }
.queue-dock .tally b { color: inherit; font-variant-numeric: tabular-nums; }
.q-submit { margin-left: auto; border: 0; border-radius: 3px; padding: .5rem 1rem;
  font-size: .88rem; font-weight: 600; cursor: pointer; background: var(--accent, #2f5d8a); color: #fff; }
.q-submit:disabled { opacity: .4; cursor: default; }
.receipt-line { font-size: .87rem; padding: .5rem .75rem; border-radius: 4px;
  background: rgba(28,112,73,.1); }
.empty-queue { padding: 2rem; text-align: center; }

/* Each control sits under the value it chooses. */
.rec-table td.option { vertical-align: top; }
.rec-table td.option .val { display: block; margin-bottom: .3rem; }
.rec-table td.option .verb { width: 100%; }
.rec-table tbody th .finding { display: block; font-weight: 400; margin-top: .2rem; }
tr.prop-note td { border-bottom: 1px solid var(--border, #eee); padding-top: 0; }
tr.prop td { border-bottom: 0; }
.outcome-line { font-size: .8rem; color: var(--accent, #2f5d8a); }
.outcome-line.on { font-weight: 600; }
.outcome-line.on.accept { color: #1c7049; }
.outcome-line.on.reject { color: var(--text-2); }
.outcome-line.on.fix { color: #8a5a12; }

/* A value a check rejected is not the ordinary path. */
.outcome-line.on.override { color: #a8452c; }
.rec-table td.option .fixval { display: block; width: 100%; margin-bottom: .3rem;
  padding: .25rem .4rem; font-size: .82rem; box-sizing: border-box; }
.queue-dock .tally.warn { color: #8a5a12; }
.flag-label { display: block; font-weight: 600; font-size: .78rem; color: var(--text); margin-top: .2rem; }

/* --- the chart-type gallery (ROADMAP item 20) -----------------------------
   The card answers, in order: can I use this, what is it, what is it for,
   what would it take, what should I know. Weight and position follow that
   order. Description and advice are different registers and never share a
   treatment. */
.type-filter { display: block; max-width: 26rem; margin: var(--s-3) 0; }
.type-filter input { width: 100%; }

.type-family { margin-bottom: var(--s-4); }
.type-family > h2 {
  font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-3); margin: 0 0 var(--s-2);
}
.type-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--s-3);
}

/* The whole card is the target, so the reason lives inside the button --
   against the thing it explains, not floating below it. */
.type-card { display: flex; flex-direction: column; gap: .35rem; }
.type-card > button {
  display: flex; flex-direction: column; gap: .3rem; width: 100%;
  padding: .75rem .85rem; text-align: left; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); color: var(--text);
}
.type-card > button:hover:not(:disabled) { border-color: var(--accent); }
.type-card > button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.type-card.on > button { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }

.type-name { font-weight: 600; line-height: 1.25; }
.type-blurb { color: var(--text-2); font-size: var(--fs-sm); line-height: 1.35; }

/* Availability is state, carried by the card. The sentence explains a state
   already seen, so it is quiet -- and it is not dimmed with the rest, because
   it is the part somebody acts on. */
.type-card.unavailable > button { cursor: not-allowed; background: var(--surface-2); }
.type-card.unavailable .type-name,
.type-card.unavailable .type-blurb { opacity: .55; }
.type-why {
  font-size: var(--fs-sm); line-height: 1.35; color: var(--text-2);
  padding-top: .35rem; border-top: 1px solid var(--border);
}

/* Advice is a recommendation about the reader's choice, not a fact about the
   chart. Different register, different colour, present only when there is
   something to advise. */
.type-advice {
  margin: 0; font-size: var(--fs-sm); line-height: 1.35;
  color: var(--tone-warn);
}
.type-card:not(.unavailable) .type-advice { color: var(--accent); }

.type-nothing { color: var(--text-3); }

/* --- the builder's shell (ROADMAP item 20) --------------------------------
   The sentence, the rail, and the chart. Only the panel changes between
   steps, so moving between them is one page with a different question in
   it rather than five pages that look alike. */
.sentence{
  /* The console has one face; the sentence earns its prominence from size
     and weight rather than from a second family nobody chose. */
  font-size: 1.24rem; font-weight: 400;
  line-height: 1.4; color: var(--text-3); margin: 0 0 var(--s-3);
  padding-bottom: var(--s-2); border-bottom: 1px solid var(--border);
}
.sentence b{ color: var(--text); font-weight: 600; }
.sentence .gap{ border-bottom: 2px dotted var(--border-strong); }
.sentence .gap.here{ color: var(--accent); border-bottom-color: var(--accent); }

.build{ display: grid; grid-template-columns: 24rem 1fr; gap: var(--s-4); }
@media (max-width: 60rem){ .build{ grid-template-columns: 1fr; } }
.build-rail{ min-width: 0; }
.build-stage{ min-width: 0; display: flex; flex-direction: column; gap: var(--s-2); }
/* `place-items: center` centred the preview by shrinking it to its content,
   so the chart measured its own box and got nothing: `render` empties the
   element before measuring, the box collapsed to zero, and every chart fell
   back to a fixed 640px however wide the pane was. Centre the rows within
   the height and let them take the full width -- what the preview is for is
   showing the chart at the size it will be read at. */
.build-stage .canvas{
  flex: 1; min-height: 22rem; border: 1px solid var(--border);
  border-radius: var(--r-md); background: var(--surface);
  display: grid; align-content: center; justify-items: stretch;
  padding: var(--s-3);
}

.steps{ display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: var(--s-3); }
.steps a, .steps .not-yet{
  font-size: var(--fs-sm); padding: .28rem .6rem; border-radius: 99px;
  border: 1px solid var(--border); color: var(--text-3); text-decoration: none;
}
.steps a.on{ border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.steps a.done{ color: var(--text-2); }
/* Shown and refused, not hidden: knowing the step exists is worth more
   than a rail that grows as you go. */
.steps .not-yet{ opacity: .45; cursor: not-allowed; }

.tree details{ border-bottom: 1px solid var(--border); }
.tree summary{ display: flex; align-items: center; gap: .4rem; padding: .3rem .5rem;
  cursor: pointer; font-size: var(--fs-sm); }
.tree summary em{ margin-left: auto; font-style: normal; color: var(--text-3);
  font-variant-numeric: tabular-nums; }
.tree .kids{ padding-left: 1.2rem; }
.tree .row.pub{ display: flex; align-items: center; gap: .4rem;
  padding: .24rem .5rem; font-size: var(--fs-sm); }

/* --- the newsroom ladder ---------------------------------------------------
   Native <details>, so the disclosure works with no JavaScript and keyboard
   and screen readers get it for free. The default triangle is replaced with
   a chevron that rotates: one control in two states rather than two glyphs
   that have to be recognised as the same thing. */
.tree summary{ list-style: none; }
.tree summary::-webkit-details-marker{ display: none; }
.tree summary::before{
  content: ""; flex: 0 0 auto; width: .58rem; height: .58rem;
  border-right: 1.6px solid var(--text-3); border-bottom: 1.6px solid var(--text-3);
  transform: rotate(-45deg); transform-origin: 60% 60%;
  transition: transform .12s ease;
}
.tree details[open] > summary::before{ transform: rotate(45deg); }
.tree summary:hover::before{ border-color: var(--text); }
.tree summary:focus-visible{ outline: 2px solid var(--accent); outline-offset: -2px; }

.tree{ border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface-2); max-height: 26rem; overflow-y: auto; }
.tree > details > summary{ background: var(--surface); position: sticky; top: 0; z-index: 1; }
.tree .row.pub:hover{ background: var(--surface); }
/* The count is what makes a facet legible rather than a wall of checkboxes. */
.tree em{ font-variant-numeric: tabular-nums; }

/* A parent whose children are partly chosen must not claim they all are. */
.tree input[type="checkbox"]:indeterminate{ opacity: .8; }

/* --- theme swatches -------------------------------------------------------
   The palette is the label. A name tells somebody nothing until they have
   chosen it and looked at the chart. */
.swatches{ display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-2);
  margin-bottom: var(--s-3); }
.swatch{ display: block; padding: .5rem; border: 1px solid var(--border);
  border-radius: var(--r-md); background: var(--surface); cursor: pointer; }
.swatch.on{ border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.swatch input{ position: absolute; opacity: 0; pointer-events: none; }
.swatch .bar{ display: flex; height: 13px; border-radius: 3px; overflow: hidden;
  margin-bottom: .35rem; }
.swatch .bar i{ flex: 1; }
.swatch .name{ font-size: var(--fs-sm); font-weight: 500; }
/* The radio is hidden, so the keyboard needs the ring on the label. */
.swatch:focus-within{ outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- value facets in the fields step --------------------------------------
   Counted, because a facet without counts is a wall of checkboxes and the
   count is what says whether narrowing leaves anything to draw. */
.values{ margin-top: .4rem; border: 1px solid var(--border);
  border-radius: var(--r-md); background: var(--surface-2); }
.values > summary{ padding: .35rem .5rem; font-size: var(--fs-sm);
  color: var(--text-3); cursor: pointer; }
/* How many of a dimension to draw, above the values themselves. A
   question about the chart rather than about the data, so it sits with
   the facet rather than inside it. */
.how-many{ display: inline-flex; align-items: center; gap: .4rem;
  margin: .3rem 0 .2rem; font-size: var(--fs-sm); color: var(--text-2);
  flex-wrap: wrap; }
/* Its own block above the Update button: it is a decision about the
   whole chart, not about the field it used to sit under. */
.how-much{ margin-top: .75rem; padding-top: .75rem;
  border-top: 1px solid var(--border); }
.how-many select{ font: inherit; font-size: var(--fs-sm); }

/* A facet arranged rather than listed: a county of publishers, a state
   of counties. The same shape the newsroom step shows, because it is the
   same sources. */
.facet-group{ border-left: 2px solid var(--border); padding-left: .5rem;
  margin: .2rem 0; }
.facet-group > summary{ cursor: pointer; font-size: var(--fs-sm);
  color: var(--text-2); padding: .15rem 0; }
.facet-group > summary:hover{ color: var(--text); }
.facet-group > summary em{ float: right; color: var(--text-3);
  font-style: normal; font-variant-numeric: tabular-nums; }
.facet-all{ color: var(--text-3); font-size: var(--fs-sm); }

/* The rule that made these rows was `.values .row`, which is the facet
   disclosure -- so the table step's column list, which is a values-list
   without a disclosure around it, got no rule at all and reflowed into one
   run-on paragraph of thirty-five checkboxes.

   Styled by the list rather than by what encloses it, and laid out in as
   many columns as fit: a corpus has dozens of fields, and a single column
   of them inside a scrolling box is a worse way to find one. */
.values-list{ max-height: 16rem; overflow-y: auto;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: .05rem .75rem; align-content: start; }
.values-list .row{ display: flex; align-items: center; gap: .45rem;
  padding: .12rem .2rem; border-radius: var(--r-sm); min-width: 0; }
.values-list .row:hover{ background: var(--surface-2, var(--surface)); }
/* Wrapped, not truncated. These labels exist to tell two questions apart
   -- "County set in" against "Counties mentioned" -- and an ellipsis in
   the middle of that is the one place a label must not be cut. */
.values-list .row .label{ overflow-wrap: anywhere; }
.values-list .row{ align-items: flex-start; }
.values-list .row input{ flex: none; }

/* The heading over each family of fields: six short lists read where one
   of forty does not. Inside a role the list is already short, so it does
   not need its own scrollbar as well as the panel's. */
.values-group{ margin: .7rem 0 .2rem; font-size: .75rem; font-weight: 600;
  letter-spacing: .02em; text-transform: uppercase; color: var(--text-3); }
.values-group:first-child{ margin-top: 0; }
.role .values-list{ max-height: none; }

/* A table's Rows and Values: one select per position, numbered, because
   the order is the meaning -- row 1 is the outermost group and row 2 nests
   inside it. The number sits beside the select rather than in its label so
   the column of numbers reads down the step as 1, 2, 3. */
.row-slot{ display: flex; align-items: center; gap: .5rem; margin: .25rem 0; }
.row-slot select{ flex: 1; min-width: 0; }
/* A column's filter sits under its select, indented past the number. */
.slot-values{ margin: 0 0 .4rem 2rem; }
.slot-n{ flex: none; width: 1.5rem; height: 1.5rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .75rem; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--text-3); background: var(--surface-3); }

/* A refusal, and the button it refuses. Disabled rather than removed: the
   button is where somebody looks to finish, and a missing one reads as a
   page that has not finished loading. */
.notice.bad{ border-left: 3px solid var(--tone-bad); }
.notice.bad strong{ display: block; color: var(--text); }
.action-bar button[disabled]{ opacity: .5; cursor: not-allowed; }
.action-bar button[disabled]:hover{ background: var(--accent); }

/* Dimensions that narrow a chart without being one, listed together under
   the slots rather than beside any of them -- they answer to no axis. */
.narrows{ margin-top: var(--s-3); padding-top: var(--s-3);
  border-top: 1px solid var(--border); }
.narrows .tree-head{ margin: 0 0 .4rem; font-size: var(--fs-sm);
  color: var(--text-3); }
.narrows details.values{ margin-bottom: .35rem; }
.narrows summary em{ color: var(--text-3); font-style: normal;
  font-size: var(--fs-sm); }
.values .row{ display: flex; align-items: center; gap: .5rem;
  padding: .26rem .5rem; font-size: var(--fs-sm); cursor: pointer; }
.values .row:hover{ background: var(--surface); }
.values em{ margin-left: auto; font-style: normal; color: var(--text-3);
  font-variant-numeric: tabular-nums; }

/* The preview's waiting state: a heading and a line under it, not one run-on
   sentence with a bold opening. */
.canvas .empty{ text-align: center; color: var(--text-3);
  max-width: 26rem; margin-inline: auto; }
.canvas .empty strong{ display: block; font-size: 1.05rem; color: var(--text-2);
  font-weight: 600; margin-bottom: .3rem; }

/* --- builder panel furniture ----------------------------------------------
   The step templates used class names the console's stylesheet never
   carried, so a list of checkboxes rendered as one run-on line. */
.build-rail .group{ margin-bottom: var(--s-3); }
.build-rail .group > h4{ margin: 0 0 .35rem; font-size: var(--fs-sm);
  font-weight: 600; }

/* One choice per line. Several on one line reads as a sentence, and a
   dataset called "VT Community News" then runs into the next one. */
.build-rail .pick{ display: flex; flex-direction: column; gap: .1rem; }
.build-rail .pick > label{ display: flex; align-items: center; gap: .5rem;
  padding: .3rem .4rem; border-radius: 5px; font-size: var(--fs-sm);
  cursor: pointer; }
.build-rail .pick > label:hover{ background: var(--surface-2); }

.build-rail .range{ display: flex; align-items: center; gap: .5rem; }
.build-rail .range input{ flex: 1; min-width: 0; }
.build-rail .range span{ color: var(--text-3); font-size: var(--fs-sm); }

.build-rail .role{ margin-bottom: var(--s-3); }
.build-rail .role > h4{ margin: 0 0 .3rem; font-size: var(--fs-sm); font-weight: 600; }
.build-rail .role .accepts{ font-weight: 400; color: var(--text-3); }
.build-rail select.var{ width: 100%; }

.build-rail .action-bar{ margin-top: var(--s-3); }
/* A gap in the sentence is not a link; it is a hole waiting to be filled. */
.sentence .gap{ text-decoration: none; cursor: default; }

/* Headings the step shell and the panels use. */
.build-rail .step-head{ margin-bottom: var(--s-2); }
.build-rail .step-head h1{ font-size: 1.2rem; margin: 0; }
.build-rail .step-head p{ margin: .2rem 0 0; color: var(--text-3); font-size: var(--fs-sm); }
.build-stage .stage-head{ display: flex; align-items: baseline;
  justify-content: space-between; gap: var(--s-3); }
.build-stage .stage-head h2{ margin: 0; font-size: 1.15rem; }
.tree .tree-head{ margin: 0 0 .4rem; font-size: var(--fs-sm); color: var(--text-3); }
.build-rail .tree-head{ margin: 0 0 .4rem; font-size: var(--fs-sm); color: var(--text-3); }
/* Hooks used only in compound selectors, named so nothing goes unstyled. */
.tree .node{ }
.tree .node.state > summary{ font-weight: 600; }
.tree .node.county > summary{ font-weight: 400; }
.tree .branch{ margin: 0; }
.tree .label.strong{ font-weight: 600; }

/* --- a picker that survives a hundred options ------------------------------
   Chips mirror the ticks rather than holding a second copy of the choice:
   the checkboxes are what submits, so removing a chip unticks its box. */
.chips{ display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .4rem; }
.chips:empty{ display: none; }
.chip{ display: inline-flex; align-items: center; gap: .3rem; font: inherit;
  font-size: var(--fs-sm); padding: .16rem .5rem; border-radius: 99px;
  border: 1px solid var(--accent); background: var(--accent-soft);
  color: var(--accent); cursor: pointer; }
.chip::after{ content: "×"; font-size: .95em; opacity: .8; }
.chip:hover{ background: var(--accent); color: var(--surface); }

.picker{ border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface); }
.picker > summary{ padding: .4rem .55rem; font-size: var(--fs-sm);
  color: var(--text-3); cursor: pointer; }
.picker-find{ width: calc(100% - 1.1rem); margin: 0 .55rem .35rem; }
.picker-list{ max-height: 14rem; overflow-y: auto; }
.picker-list .row{ display: flex; align-items: center; gap: .5rem;
  padding: .28rem .55rem; font-size: var(--fs-sm); cursor: pointer; }
.picker-list .row:hover{ background: var(--surface-2); }
.picker .notice{ padding: 0 .55rem .4rem; }
.picker .picker-count{ font-variant-numeric: tabular-nums; }
/* What the two numbers on a ladder row mean, said once. */
.tree-head .hint{ display: block; color: var(--text-3); font-size: .75rem; }

/* --- the colour theme toggle ---------------------------------------------
   Three states: an explicit light or dark, or auto, which follows the
   operating system and is what somebody gets before they choose. */
.theme-toggle{ display: inline-flex; border: 1px solid var(--border);
  border-radius: 99px; overflow: hidden; margin-right: var(--s-3); }
.theme-toggle button{ font: inherit; font-size: .78rem; cursor: pointer;
  padding: .18rem .6rem; border: 0; background: transparent;
  color: var(--text-3); }
.theme-toggle button + button{ border-left: 1px solid var(--border); }
.theme-toggle button:hover{ color: var(--text); background: var(--surface-2); }
.theme-toggle button.on{ background: var(--accent-soft); color: var(--accent);
  font-weight: 600; }
.theme-toggle button:focus-visible{ outline: 2px solid var(--accent);
  outline-offset: -2px; }
/* A subset choice needs its explanation beside it: "complete" and
   "enriched" both sound like done, and what differs is the contents. */
.build-rail .pick > label > span{ display: flex; flex-direction: column; }
.build-rail .pick em{ font-style: normal; color: var(--text-3); font-size: .78rem;
  line-height: 1.3; }

/* --- the scan bar over the review queue ------------------------------------
   When it last ran, and a way to run it. An empty queue means nothing is
   wrong or nothing has looked; without this line a reviewer cannot tell. */
.scan-bar{ display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-3); padding: .4rem .6rem; margin-bottom: var(--s-3);
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface-2); font-size: var(--fs-sm); }
.scan-bar .scan-when{ color: var(--text-2); }
.scan-bar button[disabled]{ opacity: .55; cursor: not-allowed; }

/* --- taking the table's data elsewhere ------------------------------------
   The table view is where somebody looks at the numbers, so it is where
   they will want them out. Flourish uploads a file; Datawrapper pastes. */
.dd-export{ display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin-top: var(--s-2); padding-top: var(--s-2);
  border-top: 1px solid var(--border); }
.dd-export button, .dd-export .dd-download{ font: inherit; font-size: var(--fs-sm); cursor: pointer;
  padding: .26rem .6rem; border-radius: var(--r-sm);
  border: 1px solid var(--border); background: var(--surface); color: var(--text); }
.dd-export button:hover, .dd-export .dd-download:hover{ border-color: var(--accent); color: var(--accent); }
.dd-export-note{ margin-left: auto; color: var(--text-3); font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums; }
/* The fallback when the clipboard is refused: something to copy from by
   hand, rather than a button that silently does nothing. */
.dd-copybox{ width: 100%; min-height: 6rem; font-family: var(--font-mono);
  font-size: .78rem; margin-top: .4rem; }
/* Whose data this is, under the table that shows it. Quiet, because it
   is a citation and not a heading. */
.dd-credit{ margin: var(--s-2) 0 0; font-size: .78rem; color: var(--text-3); }
.dd-credit a{ color: inherit; text-decoration: underline; }

/* Sits in the preview's own header, beside where the chart will change.
   Quiet: it is a way to ask, not the step's main action, which is Next. */
.stage-head .refresh{ font-size: .8rem; padding: .25rem .6rem;
  border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--text-2); cursor: pointer; }
.stage-head .refresh:hover{ color: var(--text); border-color: var(--accent); }

/* A label a screen reader reads and a sighted reader does not need --
   the control beside it already says what it is. Clipped rather than
   `display:none`, which would take it out of the accessibility tree with
   everything else. */
.visually-hidden{ position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden; white-space: nowrap;
  clip-path: inset(50%); border: 0; }

/* Project folders on the visuals list. A group heading inside the table
   rather than a table each, so the columns stay aligned down the page. */
/* The two things somebody starts on this page, side by side and shaped
   alike. "New visual" was a link in prose, which reads as a footnote
   rather than as the main thing the page is for -- so it takes the same
   shape as the button beside it, and `a.button` borrows the rule the
   real buttons already use rather than inventing a second look. */
.page-actions{ display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s-2); margin: 0 0 var(--s-3); }
a.button{ display: inline-block; font-family: inherit;
  font-size: var(--fs-sm); padding: 0.3rem var(--s-3);
  border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text); text-decoration: none; }
a.button:hover{ background: var(--surface-2); }

.new-folder{ display: flex; gap: .5rem; margin: 0; }
/* The three controls are one row of equal-height things, so they share
   the button's box: same font size, same padding, same border width. An
   input left to its own defaults sits a pixel or two proud of a button
   beside it, which reads as misalignment rather than as a difference. */
/* The controls, not their wrapper: `> *` also matched the form holding
   two of them and drew a border round the pair. */
.page-actions input,
.page-actions button,
.page-actions a.button{ font-size: var(--fs-sm); line-height: 1.4;
  padding: 0.3rem var(--s-3); border-width: 1px; border-style: solid;
  border-radius: var(--r-sm); box-sizing: border-box; }
/* The lighter border, not --border-strong: this is a field waiting for
   an optional name, not a control demanding attention. */
.new-folder input{ flex: 1 1 14rem; max-width: 20rem;
  border-color: var(--border); background: var(--surface); }
.new-folder input:focus{ border-color: var(--accent); }
.new-folder button{ white-space: nowrap; }

/* THE HEADING IS A BAR, so it has to lay out as one -- but the flex box
   goes on a wrapper INSIDE the cell, not on the cell.
   `display: flex` on a `th` takes it out of table layout, and the browser
   then sizes the columns without it: the spanning heading stopped
   spanning and the Visual column was pushed most of the way across the
   table, leaving every title stranded far from its own grab handle. */
.visuals-list .folder-head th{ background: var(--surface-2);
  font-size: .82rem; letter-spacing: .02em; padding: .4rem .6rem;
  border-top: 1px solid var(--border); }
.folder-head .folder-bar{ display: flex; align-items: center; gap: .4rem; }
/* The gap spaces the count now; the margin would double it. */
.visuals-list .folder-head .dim{ color: var(--text-3); font-weight: 400;
  font-variant-numeric: tabular-nums; }

/* The palette sits at the far end, away from the name and the count --
   it acts on the whole folder rather than on anything next to it, and at
   the end it is out of the way of a list meant to be read. */
.folder-head form.folder-theme{ display: inline-flex; align-items: center;
  gap: .3rem; margin-left: auto; }
.folder-head form.folder-theme-undo{ display: inline-flex; }
/* Quiet chrome, like .rename-save: one of these per folder down the page
   is a column of buttons, and none of them is the thing being read. */
/* "Theme:" is a caption, not a control. It sat inside a `th`, so it
   inherited the heading's weight and read as loudly as the folder's own
   name -- three bold words competing on one row. The palette name is the
   part that is actionable and it keeps the button. */
.folder-head .theme-label{ font-weight: 400; color: var(--text-2); }
.folder-head .theme-open,
.folder-head .theme-save,
.folder-head .folder-theme-undo button{ font: inherit; font-size: .72rem;
  padding: .1rem .45rem; white-space: nowrap; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text-2); }
.folder-head .theme-open:hover,
.folder-head .theme-save:hover,
.folder-head .folder-theme-undo button:hover{ color: var(--text);
  border-color: var(--border-strong); }
/* Open, it is a live control and stops being quiet. */
.folder-head .theme-open[aria-expanded="true"]{ color: var(--text);
  border-color: var(--border-strong); background: var(--surface-2); }
.folder-head form.folder-theme select{ font: inherit; font-size: .72rem;
  padding: .1rem .3rem; color: var(--text); background: var(--surface);
  border: 1px solid var(--border-strong); border-radius: var(--r-sm); }

/* Where a dragged visual will land. Marked on the heading, which is the
   drop target, so the feedback is where the pointer is. */
.visuals-list .folder-head.drop-target th{ outline: 2px dashed var(--accent);
  outline-offset: -2px; color: var(--text); }
.visuals-list tr[draggable]{ cursor: grab; }
.visuals-list .empty-folder td{ font-size: .82rem; }

/* The map's frame follows the newsrooms above it, so this is shut and
   empty in the ordinary case -- open only where somebody has overridden
   it, which the template does by looking at whether a place is set. */
.focus-override{ margin: var(--s-2) 0 0; border-top: 1px solid var(--border);
  padding-top: var(--s-2); }
.focus-override > summary{ cursor: pointer; font-size: .82rem;
  color: var(--text-2); }
.focus-override > summary:hover{ color: var(--text); }

/* The chevron that opens and shuts a folder. Rotated rather than swapped
   for a different character, so it reads as one control in two states. */
.folder-head .twist{ background: none; border: 0; padding: .1rem .3rem;
  margin-right: .2rem; color: var(--text-3); cursor: pointer;
  border-radius: var(--r-sm); vertical-align: middle; }
.folder-head .twist:hover{ color: var(--text); background: var(--surface); }
.folder-head .twist svg{ display: block; transform: rotate(90deg);
  transition: transform .12s ease; }
@media (prefers-reduced-motion: reduce){
  .folder-head .twist svg{ transition: none; }
}
.folder-head .twist[aria-expanded="false"] svg{ transform: rotate(0deg); }

/* Shut: the rows go, the heading stays -- and so does the drop target,
   because dragging into a folder you have put away is the ordinary case. */
tbody.folder.shut tr:not(.folder-head){ display: none; }

/* The folder name, edited where it is read. The input carries no chrome
   until it is focused, so the heading reads as a heading and turns into a
   field under the cursor rather than looking like a form all the time. */
.folder-head form.rename{ display: inline-flex; align-items: center; gap: .4rem; }
/* Sized to the name it holds, not to a column: a fixed width pushed the
   count away from the folder it counts, which read as two unrelated
   things on one row. */
.folder-head form.rename input{ font: inherit; color: inherit;
  background: none; border: 1px solid transparent; border-radius: var(--r-sm);
  padding: .1rem .3rem; width: auto; min-width: 8rem; max-width: 24rem;
  field-sizing: content; }
.folder-head form.rename input:hover{ border-color: var(--border); }
.folder-head form.rename input:focus{ border-color: var(--accent);
  background: var(--surface); }
/* The button is only useful once something has changed, and a row of them
   down the page is noise, so it appears with focus in the field. */
.rename-save{ font-size: .72rem; padding: .1rem .4rem; visibility: hidden;
  border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--text-2); cursor: pointer; }
.folder-head form.rename:focus-within .rename-save{ visibility: visible; }

/* The handle. Six dots on the left of the row is what people already
   look for on something reorderable, so it needs no caption -- and it
   keeps the drag off the row itself, where it would start on the way to
   every click and make the text unselectable. */
.grip-cell{ width: 1.6rem; padding-right: 0; }
.grip{ display: block; padding: .2rem .25rem; background: none; border: 0;
  color: var(--text-3); cursor: grab; border-radius: var(--r-sm); }
.grip:hover{ color: var(--text); background: var(--surface-2); }
.grip:active{ cursor: grabbing; }
.grip svg{ fill: currentColor; display: block; }
tr.dragging{ opacity: .5; }

/* Shown when the grip beside it is used. One of these on every row is a
   wall of identical dropdowns, and the list is meant to be read. */
.move-select{ font-size: .76rem; padding: .05rem .25rem; margin-left: .5rem;
  background: var(--surface-2); color: var(--text-2);
  border: 1px solid var(--border); border-radius: var(--r-sm); }

/* A button that has to sit in a row of links without looking like a
   button -- it creates a record, so it must be a form, but it is the same
   kind of small action as the links beside it. */
.inline-form{ display: inline; }
.inline-form button.edit-link{ background: none; border: 0; padding: 0;
  font: inherit; color: var(--accent); cursor: pointer; }
.inline-form button.edit-link:hover{ text-decoration: underline; }

/* A count that has not arrived yet. An ellipsis rather than a spinner or
   a zero: the row is already readable, and a zero would be a lie for the
   second it took to be replaced. */
.node .tally:empty::before{ content: "\2026"; opacity: .45; }

/* Two fields that are one thought -- a name and how to reach them. Side
   by side above a certain width, stacked below it. */
.files-lead{ font-size: .82rem; }
.pair{ display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-2);
  margin: 0 0 var(--s-2); }
@media (max-width: 40rem){ .pair{ grid-template-columns: 1fr; } }

/* What the preview is still waiting for, above a chart that cannot draw
   its data yet. Quiet: it is a caption on the preview, not a warning. */
.preview-note{ margin: 0 0 var(--s-2); font-size: .8rem; color: var(--text-3);
  text-align: center; }

/* Which colour the embed holds still at. Laid out in a row because the
   three choices are one decision, not three settings. */
.theme-pick{ border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: .5rem .7rem .6rem; margin: 0 0 var(--s-2);
  display: flex; flex-wrap: wrap; gap: .25rem 1rem; align-items: center; }
.theme-pick legend{ padding: 0 .3rem; font-size: .72rem; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-3); }
.theme-pick label{ display: inline-flex; align-items: center; gap: .35rem;
  font-size: .82rem; color: var(--text-2); cursor: pointer; }
.theme-pick input{ margin: 0; }

/* The embed snippet: two lines somebody copies, so it wraps rather than
   scrolling sideways out of reach. */
.embed-snippet{ white-space: pre-wrap; word-break: break-all;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: .5rem .6rem; font-size: .78rem;
  margin: 0 0 var(--s-2); }

/* The advanced sources on the new-visual form: three ways to bring data
   from somewhere else, folded away because the answer is the corpus
   almost every time. The chevron is the one the folder headings use, so
   "this opens" reads the same in both places. */
.advanced{ margin-top: .75rem; }
.advanced > summary{ display: flex; align-items: center; gap: .4rem;
  cursor: pointer; color: var(--text-2); font-size: var(--fs-sm);
  list-style: none; padding: .3rem 0; }
/* Safari draws its own triangle from the shadow DOM; ours is the one to
   keep, or the row shows two things that both mean "open". */
.advanced > summary::-webkit-details-marker{ display: none; }
.advanced > summary:hover{ color: var(--text); }
.advanced > summary svg{ flex: none; transition: transform .12s ease; }
.advanced[open] > summary svg{ transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce){
  .advanced > summary svg{ transition: none; }
}
/* The fields inside sit in the fieldset's own rhythm rather than tight
   under the summary. */
.advanced[open] > label{ margin-top: .5rem; }

/* The data panel's own top: what the page put there on the left, the way
   back on the right. `align-items: flex-start` so the button sits level
   with the first line of the prose rather than centred against three
   lines of it. */
.dd-panel-head{ display: flex; align-items: flex-start;
  justify-content: space-between; gap: 1rem;
  padding-bottom: .75rem; margin-bottom: .75rem;
  border-bottom: 1px solid var(--border); }
.dd-panel-lead{ min-width: 0; }
/* Nothing on the left is not a reason for the button to move left. */
.dd-panel-head > .dd-toggle{ flex: none; margin-left: auto; }

/* A download offered on the page: an anchor so right-click "Save as" and
   middle-click keep working, wearing the export bar's button. */
.dd-export .dd-download{ display: inline-block; text-decoration: none; }
/* The policy links Google requires on the page it treats as this app's
   home. Quiet: they are a requirement and a courtesy, not navigation. */
.site-footer{ display: flex; gap: 1rem; flex-wrap: wrap;
  padding: 2rem 0 1rem; margin-top: 2rem;
  border-top: 1px solid var(--border);
  font-size: var(--fs-sm); }
.site-footer a{ color: var(--text-3); text-decoration: none; }
.site-footer a:hover{ color: var(--text); text-decoration: underline; }

/* A date field that opens a calendar and only a calendar. The native
   control differs by browser and cannot be styled; in Firefox its month
   and year are a second view that replaces the day grid, which is the one
   view anybody wanted. */
.cal-field{ position: relative; display: inline-block; }
.cal-pop{ position: absolute; z-index: 30; top: calc(100% + .25rem); left: 0;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow-2, 0 6px 24px rgba(0,0,0,.12));
  padding: .6rem; width: 17rem; }
.cal-head{ display: flex; align-items: center; justify-content: space-between;
  gap: .5rem; margin-bottom: .45rem; }
.cal-title{ font-weight: 600; font-size: var(--fs-sm); }
.cal-step{ border: 1px solid var(--border); background: var(--surface);
  border-radius: var(--r-sm); width: 1.7rem; height: 1.7rem; line-height: 1;
  cursor: pointer; color: var(--text-2); font-size: 1rem; }
.cal-step:hover{ background: var(--bg); color: var(--text); }
.cal-grid{ display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; }
.cal-day{ text-align: center; font-size: .7rem; color: var(--text-3);
  padding: .2rem 0; }
.cal-date{ border: 0; background: transparent; cursor: pointer;
  padding: .3rem 0; border-radius: var(--r-sm); font-size: var(--fs-sm);
  color: var(--text); font-variant-numeric: tabular-nums; }
.cal-date:hover{ background: var(--bg); }
.cal-date.is-today{ box-shadow: inset 0 0 0 1px var(--border-strong); }
.cal-date.is-chosen{ background: var(--accent); color: #fff; }
.cal-foot{ display: flex; justify-content: flex-end; margin-top: .4rem; }
.cal-field :focus-visible{ outline: 2px solid var(--focus-ring); outline-offset: 1px; }

/* The second door on the sign-in page: present, and plainly the
   exception rather than the usual way in. */
.auth-or{ display: flex; align-items: center; gap: .75rem;
  margin: 1.5rem 0 1rem; color: var(--text-3); font-size: var(--fs-sm); }
.auth-or::before, .auth-or::after{ content: ""; flex: 1 1 auto;
  border-top: 1px solid var(--border); }
.auth-password p{ margin: 0; }
.auth-password input[type="email"], .auth-password input[type="text"],
.auth-password input[type="password"]{ width: 100%; }

/* The password form, folded away under a summary that reads as a link
   rather than a heading -- it is the second answer to "how do I sign in",
   and the first one is the button above it. */
/* Rows kept but not counted among the ones that matter: disabled
   accounts under the users table, folded away. */
.more-rows{ margin-top: var(--s-3); }
.more-rows > summary{ cursor: pointer; font-size: var(--fs-sm);
  color: var(--text-3); list-style: none; }
.more-rows > summary::-webkit-details-marker{ display: none; }
.more-rows > summary::before{ content: "+ "; color: var(--text-3); }
.more-rows[open] > summary::before{ content: "\2212 "; }
.more-rows > summary:hover{ color: var(--text); }
.more-rows[open] > summary{ margin-bottom: .6rem; }

.auth-more{ margin-top: 1.25rem; }
.auth-more > summary{ cursor: pointer; font-size: var(--fs-sm);
  color: var(--text-2); list-style: none; }
.auth-more > summary::-webkit-details-marker{ display: none; }
.auth-more > summary::before{ content: "+ "; color: var(--text-3); }
.auth-more[open] > summary::before{ content: "\2212 "; }
.auth-more > summary:hover{ color: var(--text); }
.auth-more[open] > summary{ margin-bottom: .75rem; }
.auth-more :focus-visible{ outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* A label above its field, and a checkbox beside its own. `form.as_p` put
   the label inline with a colon after it, which is Django's default and
   nobody's design. */
.auth-password .field{ display: flex; flex-direction: column; gap: .25rem; }
.auth-password .field-check{ display: flex; align-items: center; gap: .45rem; }
.auth-password .field-check input{ width: auto; margin: 0; }
.auth-password .field-check label{ margin: 0; }
/* Under the button, where somebody looks only once the password failed. */
.auth-password .auth-note{ align-self: flex-start; text-decoration: none; }
.auth-password .auth-note:hover{ text-decoration: underline; }

/* The custom range's two dates, shown only when the window is Custom.
   Hidden in CSS rather than removed from the markup, so the range still
   works with no script and a bookmarked ?days=custom renders it. */
.js-custom-range{ display: contents; }
.js-custom-range.hidden{ display: none; }

/* The extraction queue's four columns, fixed.

   The widths were the browser's, worked out from the content: a long
   headline took the space it wanted and every other column gave way, so
   the disposition buttons sat in a different place on every row and the
   eye had to find them again each time. A reviewer works down this table
   by pressing the same button forty times; it has to be in the same
   place.

   `table-layout: fixed` makes the widths the ones declared here rather
   than the ones the longest cell asks for, and the headline wraps inside
   its half instead of pushing. The percentages are of the table, so they
   hold at every width the breakpoint allows. */
.queue-rows { table-layout: fixed; }
.queue-rows col.story { width: 50%; }
.queue-rows col.flagged { width: 12%; }
.queue-rows col.has { width: 20%; }
.queue-rows col.decide { width: 18%; }

/* `table.rec-table tbody th` is nowrap, which is right for the record
   tables it was written for -- a field name -- and wrong for a headline.
   Here the row header IS the story, and it wraps.

   Written as `table.rec-table.queue-rows` and not `.queue-rows`: the rule
   it overrides is (0,1,3) and a class-plus-two-elements selector is
   (0,1,2), so the shorter form LOST and the headline still would not
   wrap. With the widths fixed, a headline that cannot wrap does not push
   the columns any more -- it overflows across them, which is worse and
   is what shipped. */
table.rec-table.queue-rows tbody th { white-space: normal; color: var(--text); }
table.rec-table.queue-rows tbody th .rec-read { overflow-wrap: anywhere; }
/* Nothing leaves its column, whatever it is: a URL where a headline
   should be has no space to break at. */
.queue-rows th, .queue-rows td { overflow-wrap: anywhere; }

/* The flag a row was raised on, and where the article ended up. The flag
   is the answer to "flagged as"; the status is context for it, not the
   thing itself, so it is set below and quieter. */
.queue-rows .flag { font-weight: 600; overflow-wrap: anywhere; }
.queue-rows .status-note { margin-top: .25rem; }
.queue-rows .status-note .chip { font-size: .7rem; }
/* The byline reads as part of the story block, one line below the date
   and length, so it is spaced away from them rather than crowding them. */
.queue-rows th .byline { margin-top: .25rem; }
/* The wire verdict sits under the status it qualifies and matches it. */
.queue-rows .wire-note { margin-top: .25rem; }
.queue-rows .wire-note .chip { font-size: .7rem; }

/* ===========================================================================
   A record table on a phone
   ===========================================================================
   A queue row is four columns -- the story, what was flagged, what the
   article has, and the decision -- and narrowing them is not reading them.
   At 700px each column is 175px; below that the headline wraps a word per
   line and the decision buttons are too small to hit. The table did not
   reflow, it only got narrower.

   So below 48rem the row stops being a row and becomes a card: the row
   header is its heading, each cell carries the column head it answers to,
   and the decision runs full width at the bottom where a thumb is. Above
   48rem nothing here applies and the table is exactly what it was.

   On `.rec-table` itself rather than on the two queues that exist today.
   Every record table in this console is the same shape -- a row header, a
   few facts, something to do about them -- and a queue that has not been
   built yet (discovery) should be readable on a phone because it used the
   table, not because somebody remembered a second class. What a new table
   does have to bring is `data-label` on its data cells; the stacking works
   without them and reads as a list of unlabelled values, so a test holds
   every rec-table to carrying them. */

@media (max-width: 48rem) {
  .rec-table,
  .rec-table tbody,
  .rec-table tr,
  .rec-table tbody th,
  .rec-table td {
    display: block;
  }

  /* The column heads move onto the cells, so the header row has nothing
     left to say. `colgroup` widths are meaningless once nothing is a
     column. */
  .rec-table thead,
  .rec-table colgroup {
    display: none;
  }

  .rec-table tr {
    padding: var(--s-3);
    border-bottom: 1px solid var(--border);
  }
  .rec-table tr:last-child { border-bottom: 0; }

  /* `table.rec-table th, table.rec-table td` sets the cell padding and the
     rule between cells, and it is (0,1,2). Written as `.rec-table td` this
     reset is (0,1,1) and loses: the cells kept an 11px indent and drew a
     line under each one, so a card read as four little rows -- the
     narrowed table this was meant to replace. Matched specificity, on
     purpose. */
  table.rec-table tbody th,
  table.rec-table tbody td {
    padding: 0;
    border: 0;
    white-space: normal;
  }

  /* The story is the card's heading: it identifies the row, which is what
     a scope="row" cell was already for. */
  .rec-table tbody th[scope="row"] {
    font-size: var(--fs-sm);
    color: var(--text);
    margin-bottom: var(--s-2);
  }

  /* Titles and URLs are the one thing here long enough to push the page
     sideways, and a page that scrolls sideways on a phone loses the
     decision column off the edge -- which is where this started. */
  .rec-table .rec-read,
  .rec-table .val {
    overflow-wrap: anywhere;
  }

  .rec-table td + td { margin-top: var(--s-3); }

  /* Which question the cell answers. Stacked, "Local" and "Exported
     unenriched" sit under each other with nothing saying which is the
     flag and which is the label. */
  .rec-table td[data-label]::before {
    content: attr(data-label);
    display: block;
    margin-bottom: 0.15rem;
    font-size: var(--fs-2xs);
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-3);
  }

  /* The decision, last and full width. 2.75rem is the smallest thing a
     thumb hits reliably; the buttons were 1.6rem tall. */
  .rec-table td.option { margin-top: var(--s-3); }
  .rec-table .verb {
    width: 100%;
    min-height: 2.75rem;
    justify-content: center;
    padding: var(--s-2) var(--s-3);
  }
  .rec-table .verb + .verb { margin-top: var(--s-2); }

  /* 16px, because iOS zooms the page when a field smaller than that takes
     focus, and it does not zoom back out. */
  .rec-table .fixval,
  .filter-bar select,
  .filter-bar input {
    font-size: 1rem;
    min-height: 2.75rem;
  }

  /* Three nested gutters -- main, card, row -- were 104px of a 355px
     screen before a word of the story. The row keeps its 12px, because
     that is the space that makes a headline look placed rather than
     jammed against a border; the two outer ones give theirs back. The
     card edge sits 8px off the screen and the text 32px, leaving 82% of
     the width for the story instead of 76%. */
  main { padding: var(--s-2); }
  .card { padding: var(--s-3); }

  /* The publisher and who owns it, stacked rather than pushed apart by a
     margin that has nowhere to push to. */
  .rec-head { flex-wrap: wrap; gap: var(--s-1) var(--s-2); }
  .rec-src { margin-left: 0; flex-basis: 100%; }

  /* Each filter takes a whole row rather than half of a ragged one. */
  .filter-bar { gap: var(--s-2); }
  .filter-bar label { flex: 1 1 100%; }

  /* The topbar is one non-wrapping flex row, so a narrow screen shrinks
     its items instead of moving them: the theme toggle went under its
     own content width and `overflow: hidden` -- there to round the pill
     -- clipped "Auto" to "Au". It wraps now, and the pill does not
     shrink. */
  .topbar {
    flex-wrap: wrap;
    gap: var(--s-1) var(--s-3);
    padding: var(--s-2) var(--s-3);
  }
  .theme-toggle { flex-shrink: 0; margin-right: 0; }

  /* The dock wraps, and clears the home indicator on a phone that has
     one. The queue's own bottom padding grows to match, or the last row
     sits under it. */
  .queue-dock {
    flex-wrap: wrap;
    gap: var(--s-1) var(--s-3);
    padding: var(--s-2) var(--s-3);
    padding-bottom: max(var(--s-2), env(safe-area-inset-bottom));
  }
  .q-submit { min-height: 2.75rem; }
  .queue { padding-bottom: 9rem; }
}

/* ---------------------------------------------------------------
   Place suggestions, geography queue
   --------------------------------------------------------------- */

.geo-prop { position: relative; display: flex; flex-wrap: wrap; gap: var(--s-1); }

/* THE ROW HAS TO STAY SHORT.
   Three verbs rendered with their sublabels, each followed by its own
   always-visible input, made a row 400px tall -- a page of 25 was a
   10,000px scroll for a queue whose whole point is being finishable in
   a sitting. The sublabel survives as the button's `title`, and the
   input appears only for the verb actually pressed: `_verbs.html` puts
   it immediately after its own button, so the pressed state reveals it
   with no script. */
.geo-prop .verb { flex: 1 1 auto; padding: 0.28rem 0.5rem; font-size: var(--fs-sm); }
.geo-prop .verb i { display: none; }

/* Specific enough to beat `.rec-table td.option .fixval`, which is
   0,3,1 and set `display: block` on every one of them. */
.rec-table td.option .geo-prop .geo-field { display: none; }
.rec-table td.option .geo-prop .verb[aria-pressed="true"] + .geo-field {
  display: block;
  position: relative;
  flex: 1 0 100%;
}

/* The store is hidden; the reviewer types into `.geo-entry`. */
.rec-table td.option .geo-prop .geo-field .geo-entry {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0.25rem 0.4rem;
  font: inherit;
  font-size: var(--fs-sm);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
}

/* A PLACE THAT HAS RESOLVED, AND CAN NO LONGER BE MISTYPED.
   Only chips are written to the field that submits, so a name the
   gazetteer does not have cannot be sent -- which is what used to
   return a 500 after the fact. */
.geo-chips { display: flex; flex-wrap: wrap; gap: 0.2rem; }
.geo-chips:not(:empty) { margin-bottom: 0.25rem; }

.geo-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  padding: 0.08rem 0.2rem 0.08rem 0.4rem;
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  color: var(--text);
  font-size: var(--fs-2xs);
  line-height: 1.5;
  white-space: nowrap;
}

.geo-chip-x {
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0 0.15rem;
  color: var(--text-2);
  font-size: var(--fs-sm);
  line-height: 1;
}

.geo-chip-x:hover { color: var(--tone-bad); }

/* THE CAPTURED TEXT OF A PAYWALL STUB IS THE COOKIE NOTICE.
   That is what was fetched and the reviewer should see it -- a story
   whose only text is a consent banner is exactly why the link has to be
   opened -- but it must not set the height of the row. */
.geo-excerpt {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* BELOW THE INPUT, NEVER OVER IT.
   `.geo-prop` is a flex container, so an absolutely-positioned child
   with no offsets takes its static position at the container's content
   box -- the TOP -- and the list covered the box being typed into.
   Anchored instead to the input's own wrapper, `top: 100%` is
   immediately under the input whichever verb was pressed. */
.geo-suggest {
  position: absolute;
  top: calc(100% + 2px);
  right: 0;
  left: auto;
  /* BELOW THE DOCK (z-index 20 -> raised to 40 for exactly this).
     The dock is fixed to the bottom of the viewport, so a list opened on
     one of the last rows covers Submit -- and a reviewer who has just
     made a decision presses a button that silently is not there. The
     dock is chrome: it outranks anything the table opens. */
  z-index: 30;
  margin: 0;
  padding: var(--s-1);
  list-style: none;
  min-width: 15rem;
  max-width: 22rem;
  max-height: 14rem;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-2);
}

.geo-suggest li {
  padding: 0.3rem 0.45rem;
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-sm);
  line-height: var(--lh-tight);
}

.geo-suggest li.on,
.geo-suggest li:hover { background: var(--surface-2); }

.geo-suggest li b { font-weight: 600; }
.geo-suggest li span { color: var(--text-2); }

/* An exact match and a correction are different answers, and a reviewer
   correcting a typo has to see which one they are taking. */
.geo-suggest .geo-fix,
.geo-suggest .geo-rung,
.geo-suggest .geo-home {
  font-style: normal;
  font-size: var(--fs-2xs);
  padding: 0.02rem 0.3rem;
  border-radius: var(--r-pill);
  margin-left: 0.25rem;
  white-space: nowrap;
}

.geo-suggest .geo-fix { background: var(--tone-warn-soft); color: var(--tone-warn); }
.geo-suggest .geo-home { background: var(--tone-good-soft); color: var(--tone-good); }
.geo-suggest .geo-rung { background: var(--tone-info-soft); color: var(--tone-info); }


/* A folder's contact sheet. A link rather than a button: it goes to a
   page, which is what a link means, and it stays visible when the folder
   is shut -- looking at what a project contains is the reason to open
   one, so it must not be behind opening it. */
.folder-head .folder-preview{ font-size: .72rem; white-space: nowrap; }

/* The contact sheet. A folder's charts side by side, small, so "do these
   read as one set" can be answered by looking -- which is the question a
   folder palette exists to serve and the one a list of titles cannot
   answer. */
.contact-sheet{ display: grid; gap: var(--s-4);
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
  margin-top: var(--s-4); }
.sheet-item{ margin: 0; display: flex; flex-direction: column; gap: .4rem; }
/* THE FRAME IS THE SIZE; the page inside it is not. A chart drawn at
   300px wide is not the chart -- its labels collide and its legend wraps,
   so the sheet would show a fault that does not exist. The iframe is
   rendered at a real width and scaled down, which shows the actual
   composition. */
.sheet-frame{ position: relative; overflow: hidden;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface); aspect-ratio: 4 / 3; }
.sheet-frame iframe{ position: absolute; top: 0; left: 0;
  width: 200%; height: 200%; border: 0;
  transform: scale(.5); transform-origin: top left; }
.sheet-item figcaption{ display: flex; align-items: baseline; gap: .5rem;
  font-size: .82rem; }
.sheet-item figcaption a{ font-weight: 500; }

/* The grouped table. Long-form rows nested twice: items under their group,
   groups under the subject. */
.dd-roster-bar { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 10px; }
.dd-roster-bar input[type="search"] { flex: 1 1 15rem; min-width: 0; }
.dd-roster-count { margin: 0 0 8px; color: var(--text-3); font-size: .85rem; }
/* The table is the one thing allowed wider than the panel, and it scrolls in
   its own box so the page never does. */
.dd-roster-scroll { overflow-x: auto; max-width: 100%;
  border: 1px solid var(--border); border-radius: var(--r-sm); }
.dd-roster-table { border-collapse: collapse; width: 100%; min-width: 640px; }
.dd-roster-table th { position: sticky; top: 0; z-index: 1; background: var(--surface-2);
  text-align: left; border-bottom: 1px solid var(--border-strong); padding: 0;
  white-space: nowrap; font-size: .78rem; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-3); }
.dd-roster-table th button { width: 100%; border: 0; background: none; padding: 8px 11px;
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit;
  text-align: inherit; cursor: pointer; }
.dd-roster-table th button:hover { color: var(--accent); background: var(--surface-3); }
.dd-roster-table th.num, .dd-roster-table td.num { text-align: right; }
.dd-roster-table th.num button { text-align: right; }
.dd-roster-table td { padding: 8px 11px; border-bottom: 1px solid var(--border);
  vertical-align: top; font-size: .88rem; }
.dd-roster-table td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.dd-roster-table tbody tr:hover td { background: var(--surface-2); }
/* A group reserves its own height in BOTH columns, so the group name sits
   level with the first item it owns instead of drifting down the block. */
.dd-roster-chips { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.dd-roster-grp {
  /* How many items this group holds, set per element by the renderer. A
     local custom property, not a theme token: it carries a count, not a
     colour, and defaults to one so a group with nothing set still has the
     height of its single row. */
  --n: 1;
  display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
  min-height: calc(var(--n) * 22px + (var(--n) - 1) * 4px); }
.dd-roster-chip { font-size: .76rem; line-height: 18px; padding: 1px 7px; border-radius: 999px;
  background: var(--surface-3); color: var(--text-3); white-space: nowrap;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.dd-roster-n { color: var(--text); font-variant-numeric: tabular-nums; opacity: .75; }

/* Bylines: a card per byline, one submit for the page. */
.byline-card { border: 1px solid var(--border-strong); border-radius: 8px;
  padding: var(--s-4); margin: 0 0 var(--s-4); background: var(--surface); }
.byline-head h2 { margin: 0; font-size: 1.25rem; }
.byline-facts { margin: .25rem 0; }
.byline-signal { display: inline-block; padding: .05rem .5rem; margin-right: .5rem;
  border-radius: 999px; background: var(--surface-3); font-size: .85rem; }
.byline-step { border: 0; border-top: 1px solid var(--border);
  margin: var(--s-4) 0 0; padding: var(--s-3) 0 0; min-width: 0; }
.byline-step legend { font-weight: 600; padding: 0 var(--s-2) 0 0; }
.byline-choices { display: grid; grid-template-columns: max-content 1fr;
  gap: var(--s-2) var(--s-3); align-items: center; }
.byline-choices label { grid-column: 1 / -1; }
.byline-choices label + textarea, .byline-choices label + select {
  grid-column: 1 / -1; margin-left: 1.6rem; max-width: 28rem; }
.byline-newsrooms tbody + tbody { border-top: 1px solid var(--border); }
.byline-newsrooms input[type="text"] { width: 100%; }
/* AN OPEN DRAWER BELONGS TO THE NEWSROOM ABOVE IT.
   The markup already groups them -- one `tbody` per newsroom holds its row and
   its drawer -- but nothing drew that, so an open drawer read as a sibling of
   the NEXT newsroom rather than a child of its own. With five stories showing,
   the newsroom whose stories they are is off the top of the block.
   So the whole group lifts to `--surface-2` and takes a rail down its left
   edge, unbroken from the newsroom row through the last story. Only when open:
   a closed drawer is one line and needs no help. */
.byline-newsrooms tbody:has(details[open]) > tr > td { background: var(--surface-2); }
.byline-newsrooms tbody:has(details[open]) > tr > td:first-child {
  box-shadow: inset 3px 0 0 var(--accent); }
.byline-stories > td { padding-top: 0; }
/* Indented under the rail, so the drawer starts inside its parent's edge. */
.byline-stories details { padding-left: var(--s-3); }
.byline-stories summary { cursor: pointer; }
.byline-why { margin: var(--s-3) 0 0; }
.byline-why input { width: 100%; max-width: 32rem; }
.byline-submit { position: sticky; bottom: 0; z-index: 3; display: flex;
  flex-wrap: wrap; gap: var(--s-4); align-items: center; padding: var(--s-3) 0;
  background: var(--surface); border-top: 1px solid var(--border-strong); }

/* An outlet's page: what it is now, as a short list of facts above its
   history. */
dl.facts { display: grid; grid-template-columns: max-content 1fr; gap: var(--s-1) var(--s-3);
  margin: 0 0 var(--s-4); }
dl.facts dt { color: var(--text-3); font-size: var(--fs-sm); }
dl.facts dd { margin: 0; }
.event-form label.checkbox { display: flex; gap: var(--s-2); align-items: baseline; margin: var(--s-2) 0; }
/* A retracted event stays, visibly withdrawn: what was believed is part of
   the record. */
.event-timeline tr.retracted td { color: var(--text-3); text-decoration: line-through; }
.event-timeline tr.retracted td .hint, .event-timeline tr.retracted td em { text-decoration: none; }
.story-form .name-pick { margin-top: calc(var(--s-1) * -1); }
.story-form .name-pick input { flex: 0 1 22rem; min-width: 0; }
button.danger { color: var(--tone-bad); border-color: var(--tone-bad); margin-left: auto; }

/* A colour option: the palette as a row of chips, one chosen. */
fieldset.swatches { border: 0; padding: 0; margin: 0 0 var(--s-2); display: flex; flex-wrap: wrap;
  gap: var(--s-1); align-items: center; }
fieldset.swatches legend { font-size: var(--fs-sm); color: var(--text-2); margin-bottom: var(--s-1);
  width: 100%; }
.swatch-pick { position: relative; cursor: pointer; }
.swatch-pick input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.swatch-chip { display: inline-flex; align-items: center; justify-content: center; width: 1.75rem;
  height: 1.75rem; border-radius: 50%; border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--border); }
.swatch-chip.auto { width: auto; padding: 0 var(--s-2); border-radius: 999px; font-size: var(--fs-xs);
  color: var(--text-2); background: var(--surface-2); }
.swatch-pick input:checked + .swatch-chip { box-shadow: 0 0 0 2px var(--accent); }
.swatch-pick input:focus-visible + .swatch-chip { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A RECORD ON TWO LINES (types.ChartType("table"): detail_columns). The
   record line is scanned -- date, headline, who it names; the detail line
   is read -- set back from it: smaller, lighter, on a tinted block spanning
   every column above, so the two read as one record and not as two. */
.dd-table tbody.dd-record { border-bottom: 1px solid var(--border); }
.dd-table tbody.dd-record td { border-bottom: 0; padding-top: 0.75rem; vertical-align: baseline; }
.dd-table tbody.dd-record:hover { background: var(--surface-2); }
.dd-table tbody.dd-record:hover .dd-detail-text { background: var(--surface-3); }
.dd-table td.dd-date { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-3);
  white-space: nowrap; font-variant-numeric: tabular-nums; }
.dd-table td.dd-lead { font-size: 0.9375rem; font-weight: 600; line-height: 1.35;
  width: 45%; }
/* The headline is the row's link: the table's link style, at the row's
   weight. */
.dd-table tr.dd-detail td { padding: 0.25rem var(--s-2) 0.9rem; }
.dd-detail-text { background: var(--surface-2); border-radius: 6px; padding: 0.55rem 0.75rem;
  font-size: var(--fs-sm); line-height: 1.55; color: var(--text-2); }

/* A list in a cell: its first few, then "+N more". */
.dd-items .dd-item:not(:last-child)::after { content: ";\00a0"; }
.dd-more { font: inherit; font-size: var(--fs-xs); border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-2); border-radius: 999px; padding: 0 0.45rem;
  margin-left: 0.3rem; cursor: pointer; white-space: nowrap; }
.dd-more:hover { color: var(--accent); border-color: var(--accent); }

/* A column shown as chips above the table instead of in it. */
.dd-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.dd-chip { font: inherit; font-size: var(--fs-sm); border: 1px solid var(--border);
  background: var(--surface); color: var(--text-2); border-radius: 999px; padding: 0.15rem 0.65rem;
  cursor: pointer; }
.dd-chip[aria-pressed="true"] { border-color: var(--accent); color: var(--text);
  background: var(--surface-2); }

/* A group's own count in its name: "Gray Television (42)". */
.dd-group-total { color: var(--text-2); font-weight: 400; font-variant-numeric: tabular-nums; }

/* A chart's colour key: a grid, so every swatch sits in the same place
   in its column and every label starts a fixed gap after it. */
.dd-key { display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: 0.35rem 1.25rem; margin: 0 0 var(--s-3); font-size: var(--fs-xs); }
.dd-key-item { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.dd-key-swatch { flex: none; width: 0.75rem; height: 0.75rem; border-radius: 3px; }
.dd-key-label { color: var(--text-2); line-height: 1.25; overflow-wrap: anywhere; }

/* Places step: a state's counties, several to a row, filtered by name. */
.county-grid{ display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  gap: .15rem .75rem; max-height: 22rem; overflow-y: auto; margin: .4rem 0 .8rem;
  padding: .4rem .5rem; border: 1px solid var(--border); border-radius: 4px; }
.county-grid .row{ display: flex; gap: .4rem; align-items: center; font-size: var(--fs-sm); }
.county-grid .row[hidden]{ display: none; }

/* THE LAYER SWITCH on a layered map: one fill at a time, the newsrooms on
   or off. Styled as the legend is, because it sits where the legend sits
   and is read the same way -- a row of small labelled controls. */
.dd-layers {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-3);
  margin-bottom: var(--s-2);
  font-size: var(--fs-xs);
  color: var(--text-2);
}
.dd-layers label { display: inline-flex; align-items: center; gap: 0.3rem; cursor: pointer; }
.dd-layers .dd-layers-head { color: var(--text-3); }
.dd-swatch.hatched {
  background: repeating-linear-gradient(135deg, var(--text-3) 0 1px, transparent 1px 4px);
  border: 1px solid var(--text-3);
}
.layer-group { margin-top: var(--s-2); }
.layer-row.disabled { opacity: 0.5; }
.layer-row select { margin-left: auto; font-size: var(--fs-xs); }
