/* ============================================================================
   eplankton — components.css
   Every class in CONTRACT §9 except the .chart-* block (charts.css owns that).
   ----------------------------------------------------------------------------
   Direction: "a light data console — calm chrome, loud numbers." White cards on
   a soft grey field, hairline borders, tight radii (4/6/8/12), no resting
   shadow on anything that is not an overlay, and no glow that is not a focus
   ring. The display face is Google Sans 600 in SENTENCE CASE with negative
   tracking; UPPERCASE survives only on small micro-labels (eyebrows, stat
   labels, table headers, badges) — never on the display face. Colour is spent
   on meaning: blue validated, amber uncertain, red contested. Controls that are
   not the one primary action on a screen are bordered white.

   Depends on tokens.css + base.css. Colour discipline: every value comes from a
   token; translucency is produced with color-mix() over tokens and always has a
   solid token fallback declared immediately before it.

   ONE exception, deliberate and documented:
     --specimen-bg: #fff  (scoped to .imgcell, never :root)
   PlanktoScope specimens are imaged on a white field. That white is part of the
   measurement, not chrome — the letterbox behind an `object-fit: contain`
   thumbnail must match the imaged ground, identically in both themes. Inverting
   or theming it would falsify the specimen. It is therefore a data colour, held
   locally rather than promoted to a design token.

   Additions beyond §9 (smallest things consistent with it):
     .nav-head .nav-brand .nav-fold .nav-group .nav-label .nav-item .nav-ico
     .nav-text .nav-foot
       — the sidebar's own chrome; §9 names .shell-nav but not its contents.
     .card.lift .modal.wide .stat-row modifiers, .flag  — noted inline.
     .ranklist .meterbar .queue-item (and their parts) — the three forms the
       new direction is built on: a ranked bar list in place of a decorative
       chart, a segmented "how it was settled" meter, and the rail's queue
       rows. Additive; they change no existing class.
   ========================================================================== */

/* ==========================================================================
   1. SURFACE
   ========================================================================== */

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  /* No resting shadow. Elevation in this direction means "overlay", and a card
     is not an overlay — the hairline is the whole edge treatment. */
  box-shadow: none;
  transition: border-color var(--dur-1) var(--ease);
}

.card > * { min-width: 0; }

/* A hairline-separated header row, not a tinted band. */
.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  min-height: 48px;
  padding: var(--s2) var(--s4);
  border-bottom: 1px solid var(--line);
}

.card-head .row { gap: var(--s2); }

/* Sentence case. The card title is the quiet label on a quiet card; the number
   inside the card is what should be loud. */
.card-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 14px;
  line-height: 1.25;
  text-transform: none;
  letter-spacing: -.01em;
  color: var(--text-primary);
  margin: 0;
}

/* Wraps rather than runs. A card sub is often a whole sentence of measurement
   ("3,682 below 0.80 in your 3 most recent classified samples") and the old
   nowrap pushed it straight through the card and off the page. */
.card-sub {
  min-width: 0;
  font-family: var(--font-ui);
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: normal;
  text-wrap: pretty;
}

.card-body {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--s4);
}

/* A readout restating a cross-filter pick, on its own row under the head. It
   is a sentence, not a label: the badge keeps its voice but wraps instead of
   being cut to the space beside the controls. */
.card-readout {
  padding: var(--s3) var(--s4) 0;
}

.card-readout > .badge {
  height: auto;
  min-height: 20px;
  padding: 3px var(--s2);
  line-height: 1.4;
  white-space: normal;
  overflow-wrap: anywhere;
}

.card-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s2);
  padding: var(--s3) var(--s4);
  border-top: 1px solid var(--line);
}

.card.flush > .card-body { padding: 0; }
.card.flush > .card-head { border-bottom: 1px solid var(--line); }

/* "glow" is now simply a card the page wants you to look at: the hairline goes
   accent. No bloom — the only glow left in the system is the focus ring. */
.card.glow {
  border: 1px solid var(--accent);
  box-shadow: none;
}

a.card:hover,
.card.lift:hover {
  border-color: var(--line-strong);
}

a.card:focus-visible { box-shadow: var(--ring); }

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

.sunken {
  min-width: 0;
  padding: var(--s4);
  background: var(--surface-sunken);
  border: 1px solid var(--line-faint);
  border-radius: var(--r-md);
}

.hairline {
  height: 0;
  margin: var(--s4) 0;
  border: 0;
  border-top: 1px solid var(--line);
}

.hairline.faint { border-top-color: var(--line-faint); }

/* ==========================================================================
   2. SIDEBAR CHROME  (contents of .shell-nav)
   ========================================================================== */

.nav-brand {
  display: flex;
  align-items: center;
  gap: var(--s2);
  height: 34px;
  padding: 0 var(--s2);
  margin-bottom: var(--s4);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-lg);
  text-transform: none;
  letter-spacing: var(--track-display);
  color: var(--text-primary);
  flex: 0 0 auto;
}

.nav-brand .nav-ico { color: var(--accent); }

/* The logo and the fold button (nav.js decision 6): side by side in the full
   sidebar, stacked in the rail. The row takes the logo's old bottom margin. */
.nav-head {
  display: flex;
  align-items: center;
  gap: var(--s1);
  margin-bottom: var(--s4);
  flex: 0 0 auto;
}

.nav-head .nav-brand { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }
.nav-fold { flex: 0 0 auto; color: var(--text-muted); }
.nav-fold:hover { color: var(--text-primary); }

.nav-group {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-bottom: var(--s4);
  flex: 0 0 auto;
}

.nav-label {
  padding: var(--s2) var(--s2) var(--s1);
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--text-muted);
}

.nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  height: 32px;
  padding: 0 var(--s3);
  border-radius: var(--r-md);
  color: var(--text-secondary);
  font-size: var(--fs-base);
  white-space: nowrap;
  transition: background-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease);
}

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

/* The selected rail item is a wash and a colour. Nothing else changes: no
   strip, no weight change, no shift — the rail must not twitch as you move. */
.nav-item.on {
  background: var(--accent-wash);
  color: var(--accent-text);
}

.nav-item.on::before { content: none; }

.nav-ico {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  font-family: var(--font-ui);
  font-size: var(--fs-tiny);
}

.nav-ico svg { width: 16px; height: 16px; }

.nav-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nav-count {
  margin-left: auto;
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.nav-foot {
  margin-top: auto;
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  flex: 0 0 auto;
}

/* rail: icons only */
.shell-nav.rail .nav-text,
.shell-nav.rail .nav-label,
.shell-nav.rail .nav-count { display: none; }

.shell-nav.rail .nav-item,
.shell-nav.rail .nav-brand {
  justify-content: center;
  gap: 0;
  padding: 0;
  width: 40px;
  align-self: center;
}

.shell-nav.rail .nav-foot { width: 100%; align-items: center; }

.shell-nav.rail .nav-head { flex-direction: column; gap: var(--s2); }
.shell-nav.rail .nav-head .nav-brand { flex: 0 0 auto; }

/* bottom bar: icon over micro label */
.shell-nav.bottom .nav-brand,
.shell-nav.bottom .nav-label,
.shell-nav.bottom .nav-count,
.shell-nav.bottom .nav-foot { display: none; }

.shell-nav.bottom .nav-group {
  flex-direction: row;
  align-items: center;
  justify-content: space-around;
  gap: var(--s1);
  width: 100%;
  margin: 0;
}

.shell-nav.bottom .nav-item {
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  height: 52px;
  width: auto;
  min-width: 56px;
  padding: 0 var(--s2);
  font-size: var(--fs-eyebrow);
}

.shell-nav.bottom .nav-item .nav-text {
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
}

/* The bottom bar has no room for a wash behind a stacked icon + label, so the
   one place a selection marker survives is here: a 2px accent tick on top. */
.shell-nav.bottom .nav-item.on {
  background: transparent;
  color: var(--accent-text);
}

.shell-nav.bottom .nav-item.on::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 4px;
  bottom: auto;
  width: 16px;
  height: 2px;
  margin-left: -8px;
  border-radius: var(--r-full);
  background: var(--accent);
}

@media (max-width: 1080px) and (min-width: 721px) {
  .shell-nav .nav-text,
  .shell-nav .nav-label,
  .shell-nav .nav-count { display: none; }
  .shell-nav .nav-item,
  .shell-nav .nav-brand {
    justify-content: center;
    gap: 0;
    padding: 0;
    width: 40px;
    align-self: center;
  }
  .shell-nav .nav-foot { width: 100%; align-items: center; }
}

/* ==========================================================================
   3. CONTROLS
   ========================================================================== */

/* The standard control of this direction: bordered white, sentence case, sans.
   Only the one primary action on a screen is allowed to be filled. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  height: 32px;
  padding: 0 var(--s4);
  border: 1px solid var(--line-control);
  border-radius: var(--r-md);
  background: var(--surface-1);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 13px;
  line-height: 1;
  text-transform: none;
  letter-spacing: 0;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  transition: background-color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease);
}

.btn:hover {
  background: var(--surface-hover);
  border-color: var(--line-strong);
}

.btn:active { background: var(--surface-3); }

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: .42;
  cursor: not-allowed;
  box-shadow: none;
}

.btn[disabled]:hover,
.btn[aria-disabled="true"]:hover {
  background: var(--surface-1);
  border-color: var(--line-control);
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.btn-primary:hover {
  background: var(--accent-dim);
  border-color: var(--accent-dim);
  color: var(--accent-ink);
}

.btn-primary:active { background: var(--accent-dim); border-color: var(--accent-dim); }

.btn-primary[disabled]:hover,
.btn-primary[aria-disabled="true"]:hover {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: none;
}

/* Ghost carries no border at all — it is a link that happens to be a button. */
.btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-secondary);
}

.btn-ghost:hover {
  background: var(--surface-hover);
  border-color: transparent;
  color: var(--text-primary);
}

.btn-ghost:active { background: var(--surface-3); }

.btn-ghost[disabled]:hover,
.btn-ghost[aria-disabled="true"]:hover {
  background: transparent;
  border-color: transparent;
  color: var(--text-secondary);
}

/* Destructive is a meaning, so it wears the meaning colour — as text on a wash,
   never as a red slab. */
.btn-danger {
  background: var(--status-critical-wash);
  border-color: var(--status-critical);
  color: var(--status-critical);
}

.btn-danger:hover {
  background: var(--status-critical-wash);
  background: color-mix(in srgb, var(--status-critical) 14%, var(--surface-1));
  border-color: var(--status-critical);
  color: var(--status-critical);
}

.btn-danger:active {
  background: var(--status-critical-wash);
  background: color-mix(in srgb, var(--status-critical) 20%, var(--surface-1));
}

.btn-danger[disabled]:hover,
.btn-danger[aria-disabled="true"]:hover {
  background: var(--status-critical-wash);
  color: var(--status-critical);
}

.btn-sm {
  height: 26px;
  padding: 0 var(--s3);
  font-size: var(--fs-tiny);
  border-radius: var(--r-sm);
}

.btn-lg {
  height: 38px;
  padding: 0 var(--s6);
  font-size: var(--fs-base);
}

.btn-icon {
  width: 32px;
  padding: 0;
  flex: 0 0 auto;
}

.btn-icon.btn-sm { width: 26px; }
.btn-icon.btn-lg { width: 38px; }
.btn-icon svg { width: 16px; height: 16px; }

.btn:focus-visible { box-shadow: var(--ring); }

/* --- text fields ---------------------------------------------------------- */

.input,
.select,
.textarea {
  display: block;
  width: 100%;
  min-width: 0;
  height: 32px;
  padding: 0 var(--s3);
  /* White field, hairline border — a control on a light console reads as an
     opening in the card, not as a sunken well. */
  background-color: var(--surface-1);
  border: 1px solid var(--line-control);
  border-radius: var(--r-md);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--fs-base);
  line-height: 30px;
  transition: border-color var(--dur-1) var(--ease),
              background-color var(--dur-1) var(--ease);
}

.input:hover,
.select:hover,
.textarea:hover { border-color: var(--line-strong); }

.input:focus,
.select:focus,
.textarea:focus {
  border-color: var(--accent);
  background-color: var(--surface-1);
}

.input:focus-visible,
.select:focus-visible,
.textarea:focus-visible { box-shadow: var(--ring); }

.input[disabled],
.select[disabled],
.textarea[disabled] { opacity: .45; cursor: not-allowed; }

.input.num,
.input.mono { font-family: var(--font-ui); font-variant-numeric: tabular-nums; }

.input.input-lg { height: 38px; line-height: 36px; font-size: var(--fs-lg); }

.textarea {
  height: auto;
  min-height: 92px;
  padding: var(--s2) var(--s3);
  line-height: 1.5;
  resize: vertical;
}

.select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 30px;
  cursor: pointer;
  color: var(--text-primary);
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-size: 5px 5px, 5px 5px;
  background-position: right 15px center, right 10px center;
  background-repeat: no-repeat;
}

/* --- field wrapper -------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  min-width: 0;
}

.field-label {
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--text-muted);
}

.field-hint {
  font-size: var(--fs-tiny);
  line-height: 1.45;
  color: var(--text-muted);
}

.field-error {
  display: flex;
  align-items: flex-start;
  gap: var(--s2);
  font-family: var(--font-ui);
  font-size: var(--fs-tiny);
  line-height: 1.45;
  color: var(--status-critical);
}

.field-error::before {
  content: "\2715";
  flex: 0 0 auto;
  line-height: 1.45;
}

.field.invalid .input,
.field.invalid .select,
.field.invalid .textarea { border-color: var(--status-critical); }

/* --- switch & checkbox ---------------------------------------------------- */

.switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  width: 36px;
  height: 20px;
  cursor: pointer;
}

.switch input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.switch > span {
  position: absolute;
  inset: 0;
  border: 1px solid var(--line-control);
  border-radius: var(--r-full);
  background: var(--surface-3);
  transition: background-color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease);
  pointer-events: none;
}

.switch > span::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: var(--r-full);
  background: var(--text-muted);
  transition: transform var(--dur-1) var(--ease),
              background-color var(--dur-1) var(--ease);
}

.switch input:checked + span {
  background: var(--accent);
  border-color: var(--accent);
}

.switch input:checked + span::after {
  background: var(--accent-ink);
  transform: translateX(16px);
}

.switch input:focus-visible + span { box-shadow: var(--ring); }

.checkbox {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* --- segmented ------------------------------------------------------------ */

/* Section tabs, boxed variant: one bordered white group, the selected segment
   is a wash and a colour. The group border never moves. */
.segmented {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  background: var(--surface-1);
  border: 1px solid var(--line-control);
  border-radius: var(--r-md);
}

.segmented button {
  height: 26px;
  padding: 0 var(--s3);
  border-radius: var(--r-sm);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 13px;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-secondary);
  white-space: nowrap;
  transition: background-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease);
}

.segmented button:hover { background: var(--surface-hover); color: var(--text-primary); }

.segmented button.on {
  background: var(--accent-wash);
  color: var(--accent-text);
  box-shadow: none;
}

.segmented button.on:hover { background: var(--accent-wash); color: var(--accent-text); }

/* --- search --------------------------------------------------------------- */

/* base.css lifts a focused control to z-index 3 so its ring clears its
   neighbours. Inside the box, the icon and the picker's clear button sit
   above that, or the focused input's background paints over them; isolation
   keeps those layers inside the box, so it never rises over a sticky band. */
.search {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  min-width: 0;
  flex: 1 1 180px;
  max-width: 340px;
}

/* The flex above is for a toolbar row. In a column (.stack) the same basis
   would be a 180px height, with the input floating in the middle of it. */
.stack > .search { flex: 0 0 auto; }

.search .input { padding-left: 30px; }

.search::before {
  content: "";
  position: absolute;
  z-index: 4;
  left: 10px;
  top: 50%;
  width: 9px;
  height: 9px;
  margin-top: -6px;
  border: 1.5px solid var(--text-muted);
  border-radius: var(--r-full);
  pointer-events: none;
}

.search::after {
  content: "";
  position: absolute;
  z-index: 4;
  left: 18px;
  top: 50%;
  width: 5px;
  height: 1.5px;
  margin-top: 2px;
  background: var(--text-muted);
  transform: rotate(45deg);
  transform-origin: left center;
  pointer-events: none;
}

/* --- chips & tabs --------------------------------------------------------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  height: 24px;
  padding: 0 var(--s3);
  border: 1px solid var(--line-control);
  border-radius: var(--r-full);
  background: var(--surface-1);
  color: var(--text-secondary);
  font-family: var(--font-ui);
  font-size: 11.5px;
  /* Chips carry taxon names and measured values — sentence case, because a
     genus is not a shout. */
  text-transform: none;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease);
}

.chip:hover { border-color: var(--line-strong); color: var(--text-primary); }

.chip.on {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--accent-text);
}

.chip .swatch { margin-right: 0; }

/* The population chip under a sample's title (components/population.js): a
   statement, not a control, so it neither hovers nor points. */
.pop-chip {
  align-self: flex-start;
  width: fit-content;
  max-width: 100%;
  cursor: help;
}
.pop-chip:hover { border-color: var(--line-control); color: var(--text-secondary); }
.pop-chip-arrow { color: var(--text-muted); }

.tabs {
  display: flex;
  align-items: stretch;
  gap: var(--s5);
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  scrollbar-width: none;
}

.tabs::-webkit-scrollbar { display: none; }

/* Section tabs, borderless variant: no box, no wash — the active tab is named
   by a 2px accent underline sitting on the row's hairline. */
.tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  height: 34px;
  padding: 0 2px;
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 13px;
  text-transform: none;
  letter-spacing: 0;
  white-space: nowrap;
  transition: color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease);
}

.tab:hover { color: var(--text-primary); }

.tab.on {
  color: var(--text-primary);
  border-bottom-color: var(--accent);
}

/* --- dropzone ------------------------------------------------------------- */

.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  min-height: 260px;
  padding: var(--s8) var(--s5);
  text-align: center;
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  color: var(--text-secondary);
  cursor: pointer;
  transition: border-color var(--dur-2) var(--ease),
              background-color var(--dur-2) var(--ease),
              color var(--dur-2) var(--ease);
}

.dropzone:hover { border-color: var(--text-muted); }

.dropzone .display,
.dropzone .display-sm { color: var(--text-primary); }

.dropzone .eyebrow { color: var(--text-muted); }

.dropzone.over {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-wash);
  color: var(--text-primary);
}

.dropzone.over .eyebrow { color: var(--accent-text); }

.dropzone:focus-visible { box-shadow: var(--ring); }

/* ==========================================================================
   4. DATA — stat tiles
   ========================================================================== */

.stat {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  min-width: 0;
  padding: var(--s4);
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}

.stat[data-severity] {
  border-left: 2px solid var(--severity-color, var(--line-strong));
}

/* The tile is the loudest thing on the page, and all of the loudness is in the
   number: a quiet mono label above, 28px of tabular sans below.
   A status pill shares the label's line while both fit, and wraps under it
   when they don't, so the pill never cuts the label short ("QUALITY S…").
   The label alone is still cut with an ellipsis on a tile narrower than it. */
.stat-label {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1) var(--s2);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.stat-value {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--ds-lg);
  line-height: 1.06;
  text-transform: none;
  letter-spacing: var(--track-display);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  color: var(--text-primary);
  min-width: 0;
}

/* A face of words (a withheld figure's placeholder) at the smaller display
   size, so it takes two lines rather than four. */
.stat-value.is-words { font-size: var(--ds-sm); line-height: 1.25; }

.stat-unit {
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-muted);
  white-space: nowrap;
}

.stat-delta {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.stat-delta.up   { color: var(--status-good); }
.stat-delta.down { color: var(--status-critical); }

/* colour is never alone — the arrow glyph carries the direction too */
.stat-delta.up::before   { content: "\25B2"; font-size: 8px; line-height: 1; }
.stat-delta.down::before { content: "\25BC"; font-size: 8px; line-height: 1; }

/* .stat is a column flex container, whose default stretch would otherwise
   pull this to the tile's full width — a border+background box with all
   that empty trailing space looks broken, so it keeps its own content size. */
.stat-badge { align-self: flex-start; max-width: 100%; }

/* A tile's mark is read, not glanced at ("not one power law, near refusal"
   is longer than a narrow tile): it wraps inside its own box rather than
   being clipped mid-word by .badge's nowrap. */
.stat-badge .badge {
  height: auto;
  min-height: 20px;
  padding-block: 3px;
  line-height: 1.3;
  white-space: normal;
}

/* The engine's sentences name export columns (`sample_concentration_factor`),
   one unbreakable word wider than a tile in a row of six: they wrap anywhere
   rather than run past the card's edge. */
.stat-hint {
  font-size: var(--fs-tiny);
  line-height: 1.4;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* "(en anglais)" after a server sentence the page could not translate
   (core/say.js, EPL-2685): a remark on the words, quieter than they are,
   italic so it still reads apart inside an already muted caption. */
.aside {
  font-size: 0.92em;
  font-style: italic;
  color: var(--text-muted);
  white-space: nowrap;
}

/* The value's own continuation (its range) — never a caption, so it keeps
   more weight than .stat-hint even though both are small (GH #531). */
.stat-subline {
  font-size: var(--fs-tiny);
  line-height: 1.4;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.stat-why summary {
  font-size: var(--fs-tiny);
  color: var(--text-muted);
  cursor: pointer;
  width: fit-content;
}

.stat-why[open] summary { color: var(--text-primary); }

.stat-why p {
  margin: var(--s2) 0 0;
  font-size: var(--fs-tiny);
  line-height: 1.4;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.stat-spark { margin-top: var(--s2); height: 28px; }

.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--s3);
  min-width: 0;
}

.stat-row.tight { gap: var(--s2); }

@media (max-width: 640px) {
  .stat-row { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
  .stat { padding: var(--s3); }
  .stat-value { font-size: var(--ds-md); }
}

/* ==========================================================================
   5. DATA — table
   ========================================================================== */

.table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--fs-small);
  color: var(--text-secondary);
}

/* A sticky mono label row on the grey surface, then hairline rows. */
.table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: var(--s2) var(--s3);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--text-muted);
  text-align: left;
  white-space: nowrap;
}

.table tbody td {
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--line-faint);
  vertical-align: middle;
}

.table tbody tr:last-child td { border-bottom: 0; }

.table tbody tr { transition: background-color var(--dur-1) var(--ease); }

.table tbody tr:hover td { background: var(--surface-hover); }

.table tbody tr.on td { background: var(--accent-wash); }

.table tbody tr[tabindex]:focus-visible { box-shadow: var(--ring); }

.table th.num,
.table td.num {
  text-align: right;
  font-family: var(--font-ui);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "zero" 1;
  letter-spacing: -.01em;
  color: var(--text-primary);
  white-space: nowrap;
}

.table td.strong { color: var(--text-primary); }
.table td.dim    { color: var(--text-muted); }

.table caption {
  padding: var(--s2) var(--s3);
  text-align: left;
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--text-muted);
}

.table.dense thead th,
.table.dense tbody td {
  padding: var(--s1) var(--s2);
  font-size: var(--fs-tiny);
}

.table.rows tbody tr:nth-child(even) td { background: var(--surface-2); }
.table.rows tbody tr:hover td { background: var(--surface-hover); }

/* sortable headers */
.th-sort {
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}

.th-sort::after {
  content: "\25B4\25BE";
  display: inline-block;
  margin-left: var(--s2);
  font-size: 8px;
  letter-spacing: -2px;
  opacity: .35;
  vertical-align: middle;
}

.th-sort:hover { color: var(--text-secondary); }
.th-sort:hover::after { opacity: .6; }

.th-sort.asc,
.th-sort.desc { color: var(--text-primary); }

.th-sort.asc::after {
  content: "\25B4";
  opacity: 1;
  color: var(--accent);
  letter-spacing: 0;
}

.th-sort.desc::after {
  content: "\25BE";
  opacity: 1;
  color: var(--accent);
  letter-spacing: 0;
}

/* Inline proportional bar behind a numeric cell — set --bar: 0..1. The bar is
   a pale ramp step so the number stays the readable thing on top of it. */
.bar-cell {
  position: relative;
  isolation: isolate;
  color: var(--text-primary);
}

.bar-cell::before {
  content: "";
  position: absolute;
  left: 0;
  top: 3px;
  bottom: 3px;
  z-index: -1;
  width: calc(var(--bar, 0) * 100%);
  min-width: 0;
  border-radius: var(--r-sm);
  background: var(--seq-200);
}

/* ==========================================================================
   5b. DATA — ranked bar list, settlement meter, queue rows

   The three forms this direction is built on. A ranked bar list says what a
   pie chart would have said, in less ink and in reading order; the meter says
   how a set of identifications was settled; the queue row is what the rail
   lists. All three are additive — no existing class changes.
   ========================================================================== */

/* --- ranked bar list ------------------------------------------------------ */

.ranklist {
  display: flex;
  flex-direction: column;
  min-width: 0;
  width: 100%;
}

/* the column caption — a mono micro-label, same voice as a table header */
.ranklist-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  height: 24px;
  border-bottom: 1px solid var(--line);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--text-muted);
  white-space: nowrap;
  min-width: 0;
}

.ranklist-row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  width: 100%;
  height: 30px;
  min-width: 0;
  padding: 0;
  text-align: left;
  border-bottom: 1px solid var(--line-faint);
  transition: background-color var(--dur-1) var(--ease);
}

.ranklist-row:last-child { border-bottom: 0; }

.ranklist-row:hover { background: var(--surface-hover); }

.ranklist-row:focus-visible { box-shadow: var(--ring); z-index: 3; }

.ranklist-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-base);
  color: var(--text-primary);
}

.ranklist-value {
  flex: 0 0 auto;
  font-family: var(--font-ui);
  font-size: var(--fs-tiny);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* the rank itself: a 2px rule along the bottom of the row, --v is 0–100% */
.ranklist-bar {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: var(--v, 0%);
  max-width: 100%;
  background: var(--seq-400);
  transition: width var(--dur-3) var(--ease);
  pointer-events: none;
}

/* --- settlement meter ----------------------------------------------------- */

/* "How were these identifications settled?" — one 12px bar, one segment per
   outcome, carved by a 3px card-coloured gap. Segment colour arrives inline
   as --seg so the caller can spend the MEANING tokens on it. */
.meterbar {
  display: flex;
  align-items: stretch;
  gap: 3px;
  width: 100%;
  height: 12px;
  min-width: 0;
  border-radius: var(--r-sm);
  background: var(--surface-1);
  overflow: hidden;
}

/* Shrinkable on purpose: the segments are meant to sum to 100%, and the gaps
   between them are extra. Letting them shrink pays for the gaps out of the
   segments in proportion, instead of clipping the last one off the end. */
.meterbar-seg {
  flex: 0 1 auto;
  min-width: 0;
  height: 100%;
  width: var(--v, 0%);
  background: var(--seg, var(--series-other));
  transition: width var(--dur-3) var(--ease);
}

.meterbar-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s4);
  min-width: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  color: var(--text-secondary);
}

.meterbar-legend > * {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.meterbar-legend .swatch {
  width: 8px;
  height: 8px;
  border-radius: var(--r-full);
  background: var(--seg, var(--swatch, currentColor));
}

.meterbar-legend .num { color: var(--text-primary); }

/* --- queue rows ----------------------------------------------------------- */

.queue-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  width: 100%;
  min-height: 30px;
  padding: 0 var(--s2);
  border-radius: var(--r-md);
  color: var(--text-secondary);
  font-size: var(--fs-base);
  text-align: left;
  white-space: nowrap;
  min-width: 0;
  transition: background-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease);
}

.queue-item > :first-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.queue-item > :last-child:not(:first-child) {
  flex: 0 0 auto;
  font-family: var(--font-ui);
  font-size: var(--fs-tiny);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.queue-item:hover {
  background: var(--surface-hover);
  color: var(--text-primary);
}

.queue-item.on {
  background: var(--accent-wash);
  color: var(--accent-text);
}

/* --- pick rows ------------------------------------------------------------ */

/* One sample per line in the "Attach a sample" picker (feed.js): fingerprint,
   name over meta, and the Attach chip at the right. The row is a .card, whose
   own flex runs as a column, so the direction is restated here. */
.pick-list { min-height: 200px; }

.card.pick-row {
  flex-direction: row;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  padding: var(--s2) var(--s3);
  text-align: left;
}

.pick-row-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
}

.pick-row-name {
  color: var(--text-primary);
  font-size: var(--fs-small);
}

.pick-row-meta { font-size: var(--fs-micro); }

.pick-row > .chip { flex: 0 0 auto; }

/* ==========================================================================
   6. DATA — badges, pills, confidence, legend, kv
   ========================================================================== */

.pill,
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  height: 20px;
  padding: 0 var(--s2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-secondary);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  line-height: 1;
  letter-spacing: .04em;
  /* Uppercase survives here because a badge is a mono micro-label, the same
     voice as an eyebrow — never the display face. */
  text-transform: uppercase;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
}

.pill {
  height: 22px;
  padding: 0 var(--s3);
  border-color: var(--line-control);
  border-radius: var(--r-full);
  background: var(--surface-2);
  letter-spacing: .04em;
}

.pill[data-state]::before {
  content: "";
  width: 6px;
  height: 6px;
  margin-right: var(--s1);
  border-radius: var(--r-full);
  background: var(--state-color, var(--text-muted));
}

.badge-neutral {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--text-secondary);
}

.badge-good {
  background: var(--status-good-wash);
  border-color: var(--status-good);
  color: var(--status-good);
}

.badge-warning {
  background: var(--status-warning-wash);
  border-color: var(--status-warning);
  color: var(--status-warning);
}

.badge-serious {
  background: var(--status-serious-wash);
  border-color: var(--status-serious);
  color: var(--status-serious);
}

.badge-critical {
  background: var(--status-critical-wash);
  border-color: var(--status-critical);
  color: var(--status-critical);
}

/* status is never colour-alone: every status badge carries a glyph + its label */
.badge-good::before     { content: "\2713"; color: var(--status-good); }
.badge-warning::before  { content: "!"; font-weight: 700; color: var(--status-warning); }
.badge-serious::before  { content: "\25B2"; font-size: 8px; color: var(--status-serious); }
.badge-critical::before { content: "\2715"; color: var(--status-critical); }

/* On the white ground the yellow/orange status hues are too faint to carry a
   label, so the label reverts to --text-* and the status hue stays on the
   border, wash and glyph. Same tokens, legible in both modes. */
:root[data-theme="light"] .badge-good,
:root[data-theme="light"] .badge-warning,
:root[data-theme="light"] .badge-serious,
:root[data-theme="light"] .badge-critical { color: var(--text-primary); }

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]):not([data-theme="light"]) .badge-good,
  :root:not([data-theme="dark"]):not([data-theme="light"]) .badge-warning,
  :root:not([data-theme="dark"]):not([data-theme="light"]) .badge-serious,
  :root:not([data-theme="dark"]):not([data-theme="light"]) .badge-critical { color: var(--text-primary); }
}

/* EPL-440: the mark for a figure `server/metrics.py` computes and the
   careful engine does not (yet) ratify — compute/eplankton/unproduced.py's
   seven claims, plus diversity and the Fingerprint, which are not in its
   vocabulary at all. Neutral like .badge-neutral on purpose: this is not a
   severity, so it never borrows a status hue. The glyph is a hollow box, the
   opposite of .badge-good's tick, but the label carries the meaning alone. */
.badge-unchecked {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--text-secondary);
}

.badge-unchecked::before { content: "\2610"; font-size: 11px; }

/* The tag links to /methods#not-checked, which says what it means. */
a.badge-unchecked { text-decoration: none; }
a.badge-unchecked:hover { color: var(--text-primary); border-color: var(--line-strong); }

/* the CC BY-NC compliance chip — this one carries a licence warning, so it
   wraps rather than clipping when the viewport gets tight */
.badge-staging {
  height: auto;
  min-height: 22px;
  padding: 2px var(--s2) 2px var(--s1);
  gap: var(--s2);
  background: var(--status-warning-wash);
  border: 1px solid var(--status-warning);
  border-radius: var(--r-sm);
  color: var(--status-warning);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  line-height: 1.35;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-align: left;
  white-space: normal;
  overflow: visible;
}

.badge-staging::before {
  content: "!";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 13px;
  height: 13px;
  margin-top: 1px;
  color: var(--status-warning);
  border: 1px solid var(--status-warning);
  border-radius: var(--r-full);
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
}

:root[data-theme="light"] .badge-staging { color: var(--text-primary); }

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]):not([data-theme="light"]) .badge-staging {
    color: var(--text-primary);
  }
}

a.badge-staging:hover {
  border-color: var(--status-warning);
  box-shadow: 0 0 0 2px var(--status-warning-wash);
}

/* --- confidence ----------------------------------------------------------- */

.conf {
  display: inline-flex;
  flex-direction: column;
  gap: 3px;
  min-width: 46px;
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

.conf.inline {
  flex-direction: row;
  align-items: center;
  gap: var(--s2);
}

.conf.inline .conf-bar { width: 36px; }

/* 3px track; fill width is the confidence; colour steps through the blue ramp,
   never red/green (that space belongs to --status-*). Low → high is
   --seq-300 → --seq-400 → --seq-500: more ink, not more light. */
.conf-bar {
  --cb-lo: var(--seq-300);
  --cb-mid: var(--seq-400);
  --cb-hi: var(--seq-500);
  position: relative;
  display: block;
  width: 100%;
  height: 3px;
  border-radius: var(--r-full);
  background: var(--line);
  overflow: hidden;
}

.conf-bar > i,
.conf-bar > span {
  display: block;
  height: 100%;
  width: calc(var(--conf, 0) * 100%);
  border-radius: inherit;
  background: var(--cb-mid);
  transition: width var(--dur-2) var(--ease);
}

.conf-bar.low > i,      .conf-bar.low > span,
.conf-bar[data-band="low"] > i,  .conf-bar[data-band="low"] > span  { background: var(--cb-lo); }

.conf-bar.mid > i,      .conf-bar.mid > span,
.conf-bar[data-band="mid"] > i,  .conf-bar[data-band="mid"] > span  { background: var(--cb-mid); }

.conf-bar.high > i,     .conf-bar.high > span,
.conf-bar[data-band="high"] > i, .conf-bar[data-band="high"] > span { background: var(--cb-hi); }

/* (The former light-mode override of this ramp is gone: light is the primary
   theme now, so the band steps above ARE the light values, and the same three
   ramp steps read correctly on the dark surface — --seq-* is not re-stepped
   per theme.) */

/* --- legend --------------------------------------------------------------- */

.legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s4);
  min-width: 0;
}

.legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  color: var(--text-secondary);
  white-space: nowrap;
  min-width: 0;
}

.legend-item .num { margin-left: var(--s1); color: var(--text-primary); }

.legend-item.dim { opacity: .45; }

.swatch {
  flex: 0 0 auto;
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--swatch, currentColor);
}

.swatch.square { width: 10px; height: 10px; border-radius: 2px; }

/* EPL-1182: a composite encoding, not a 9th categorical hue — the one
   TAXON_GROUPS member (Artefact) whose groupColor() collides with the
   generic --series-other fold reads apart from a true "Other" swatch via
   this diagonal overlay instead. See engine.groupHatched(). */
.swatch.hatched {
  background: var(--swatch, currentColor);
  background-image: repeating-linear-gradient(135deg, var(--surface-1) 0 1.2px, transparent 1.2px 3px);
}

.swatch.dot { width: 9px; height: 9px; border-radius: var(--r-full); }

.swatch.line { width: 14px; height: 2px; border-radius: var(--r-full); }

.swatch.area {
  width: 14px;
  height: 9px;
  border-radius: 2px;
  opacity: .55;
  border-top: 2px solid var(--swatch, currentColor);
}

/* --- key / value list ----------------------------------------------------- */

.kv {
  display: grid;
  grid-template-columns: minmax(88px, max-content) minmax(0, 1fr);
  gap: var(--s2) var(--s4);
  align-items: baseline;
  margin: 0;
  min-width: 0;
}

.kv dt {
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--text-muted);
  white-space: nowrap;
}

.kv dd {
  margin: 0;
  min-width: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-tiny);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

.kv dd.prose {
  font-family: var(--font-body);
  font-size: var(--fs-small);
  color: var(--text-secondary);
}

.kv.rows { grid-template-columns: minmax(0, 1fr); gap: var(--s1); }

@media (max-width: 640px) {
  .kv { grid-template-columns: minmax(0, 1fr); gap: var(--s1) 0; }
  .kv dd { margin-bottom: var(--s2); }
}

/* --- quality flags (severity strip) --------------------------------------- */

.flag {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-left: 2px solid var(--severity-color, var(--line-strong));
  border-radius: var(--r-md);
  background: var(--surface-2);
  min-width: 0;
}

.flag-body { min-width: 0; display: flex; flex-direction: column; gap: var(--s1); }

.flag-label { font-size: var(--fs-small); color: var(--text-primary); }

.flag-detail { font-size: var(--fs-tiny); line-height: 1.5; color: var(--text-secondary); }

/* ==========================================================================
   7. MEDIA — specimen grid, avatars, fingerprint chip
   ========================================================================== */

.imgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--cell, 104px), 1fr));
  gap: var(--s2);
  min-width: 0;
}

.imgcell {
  /* the imaging ground — see the note at the top of this file */
  --specimen-bg: #fff;
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  padding: 0;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  cursor: pointer;
  outline: none;
  transition: border-color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease);
}

.imgcell:hover { border-color: var(--line-strong); }

.imgcell img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* specimens are imaged on white — never inverted, never filtered */
  background: var(--specimen-bg);
  filter: none;
  -webkit-user-drag: none;
  user-select: none;
}

/* A thumbnail smaller than the square it stands for (components/thumbfit.js),
   padded back to that square's scale on the specimen's white. A percentage
   padding is of the tile's width, which is the picture's own box here. */
.imgcell img[data-thumb-fill] {
  padding: calc((1 - var(--thumb-fill, 1)) * 50%);
}

.imgcell.sel {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  z-index: 1;
}

.imgcell:focus-visible {
  box-shadow: var(--ring);
  z-index: 3;
}

.imgcell-meta {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s1);
  padding: 3px var(--s1) 3px var(--s2);
  background: var(--bg-deep);
  background: color-mix(in srgb, var(--bg-deep) 78%, transparent);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  line-height: 1.2;
  color: var(--text-primary);
  min-width: 0;
}

.imgcell-meta > span:first-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.imgcell-conf {
  position: absolute;
  right: 3px;
  bottom: 3px;
  padding: 1px 4px;
  border-radius: var(--r-sm);
  background: var(--bg-deep);
  background: color-mix(in srgb, var(--bg-deep) 80%, transparent);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.imgcell-meta .imgcell-conf {
  position: static;
  background: transparent;
  padding: 0;
  color: var(--text-secondary);
}

.imgcell-flag {
  position: absolute;
  left: 3px;
  top: 3px;
  width: 6px;
  height: 6px;
  border-radius: var(--r-full);
  background: var(--state-color, var(--accent));
}

.imgcell-speck {
  position: absolute;
  left: 3px;
  top: 3px;
  max-width: calc(100% - 6px);
  padding: 1px 4px;
  border-radius: var(--r-sm);
  background: var(--bg-deep);
  background: color-mix(in srgb, var(--bg-deep) 82%, transparent);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}

.thumbstrip {
  display: flex;
  gap: var(--s2);
  overflow-x: auto;
  padding-bottom: var(--s2);
  min-width: 0;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

.thumbstrip .imgcell {
  flex: 0 0 auto;
  width: var(--cell, 76px);
}

/* --- avatar --------------------------------------------------------------- */

.avatar {
  --avatar-size: 32px;
  --avatar-ring: var(--line-strong);
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: var(--r-full);
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--avatar-ring);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: calc(var(--avatar-size) * .44);
  line-height: 1;
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--text-secondary);
  user-select: none;
}

.avatar svg,
.avatar img { width: 100%; height: 100%; display: block; }

.avatar-sm { --avatar-size: 24px; }
.avatar-lg { --avatar-size: 40px; }

a.avatar:hover { box-shadow: inset 0 0 0 1px var(--accent); }

/* --- fingerprint chip ----------------------------------------------------- */

.fingerprint-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--r-sm);
  background: var(--surface-1);
  border: 1px solid var(--line);
  overflow: hidden;
  line-height: 0;
}

.fingerprint-chip svg { width: 100%; height: 100%; display: block; }

a.fingerprint-chip:hover { border-color: var(--accent); }

/* A project's name where it links to the project (components/projects.js). */
a.p-link { color: var(--accent); }
a.p-link:hover { text-decoration: underline; }

/* ==========================================================================
   8. FEEDBACK — toast, modal, empty, skeleton, spinner, progress, stages
   ========================================================================== */

.toast-host {
  position: fixed;
  right: var(--s5);
  bottom: var(--s5);
  z-index: 120;
  display: flex;
  flex-direction: column-reverse;
  gap: var(--s2);
  max-width: min(380px, calc(100vw - var(--s7)));
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--shadow-3);
  color: var(--text-primary);
  font-size: var(--fs-base);
  line-height: 1.45;
  animation: ep-toast-in var(--dur-2) var(--ease) both;
}

.toast::before {
  content: "\00B7";
  flex: 0 0 auto;
  font-family: var(--font-ui);
  font-size: var(--fs-tiny);
  line-height: 1.6;
  color: var(--accent);
}

.toast.good { border-left-color: var(--status-good); }
.toast.warn { border-left-color: var(--status-warning); }
.toast.bad  { border-left-color: var(--status-critical); }

.toast.good::before { content: "\2713"; color: var(--status-good); }
.toast.warn::before { content: "!"; color: var(--status-warning); font-weight: 700; }
.toast.bad::before  { content: "\2715"; color: var(--status-critical); }

.toast-close {
  margin-left: auto;
  flex: 0 0 auto;
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: var(--fs-tiny);
}

.toast-close:hover { color: var(--text-primary); }

@keyframes ep-toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

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

.modal-host {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s5);
  /* The ink of --text-primary at 32%, with just enough blur to say "the page
     behind is out of play". The scrim is the same in both themes, so it is
     written out rather than derived from a token that flips. */
  background: rgba(20, 24, 29, .32);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  animation: ep-fade-in var(--dur-1) var(--ease) both;
}

.modal {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 520px;
  max-height: min(86vh, 860px);
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  animation: ep-modal-in var(--dur-2) var(--ease) both;
}

.modal.wide { max-width: 920px; }

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  min-height: 48px;
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-lg);
  text-transform: none;
  letter-spacing: var(--track-display);
  color: var(--text-primary);
  flex: 0 0 auto;
}

.modal-body {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--s5) var(--s4);
  overflow-y: auto;
  color: var(--text-secondary);
  font-size: var(--fs-base);
  line-height: 1.55;
}

.modal-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s2);
  padding: var(--s3) var(--s4);
  border-top: 1px solid var(--line);
  background: var(--surface-2);
  flex: 0 0 auto;
}

.modal-foot[hidden] { display: none; }

/* The bell's rows (nav.js, EPL-2476). The link carries the panel's padding,
   so the whole row still follows it; the ✕ sits beside it, not inside. */
.panel.notif-row {
  display: flex;
  align-items: flex-start;
  padding: 0;
}

.notif-main {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--s4);
  border-radius: inherit;
}

.notif-row > .btn { margin: var(--s3) var(--s3) 0 0; }

.notif-clear-ask {
  font-size: var(--fs-small);
  color: var(--text-secondary);
}

@keyframes ep-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes ep-modal-in {
  from { opacity: 0; transform: translateY(8px) scale(.99); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 640px) {
  .modal-host { padding: var(--s3); align-items: flex-end; }
  .modal { max-height: 88vh; }
  .toast-host { left: var(--s3); right: var(--s3); bottom: calc(74px + var(--s2)); max-width: none; }
}

/* --- empty ---------------------------------------------------------------- */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  min-height: 200px;
  padding: var(--s9) var(--s5);
  text-align: center;
}

.empty-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--ds-sm);
  line-height: 1.2;
  text-transform: none;
  letter-spacing: var(--track-display);
  color: var(--text-primary);
}

.empty-hint {
  max-width: 52ch;
  font-size: var(--fs-base);
  line-height: 1.6;
  color: var(--text-muted);
}

.empty .btn { margin-top: var(--s2); }

/* --- skeleton & spinner --------------------------------------------------- */

.skeleton {
  position: relative;
  display: block;
  min-height: 12px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  background-image: linear-gradient(
    90deg,
    var(--surface-2) 0%,
    var(--surface-3) 46%,
    var(--surface-2) 92%
  );
  background-size: 240% 100%;
  background-repeat: no-repeat;
  animation: ep-shimmer 1.5s linear infinite;
}

.skeleton.text { height: 12px; margin: var(--s1) 0; }
.skeleton.line { height: 8px; }
.skeleton.block { height: 120px; border-radius: var(--r-md); }

@keyframes ep-shimmer {
  from { background-position: 140% 0; }
  to   { background-position: -60% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; background-image: none; background: var(--surface-2); }
  .toast, .modal, .modal-host { animation: none; }
}

.spinner {
  display: inline-block;
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  border: 2px solid var(--line-strong);
  border-top-color: var(--accent);
  border-radius: var(--r-full);
  animation: ep-spin .8s linear infinite;
}

.spinner.lg { width: 22px; height: 22px; border-width: 2px; }

@keyframes ep-spin {
  to { transform: rotate(360deg); }
}

/* --- progress ------------------------------------------------------------- */

.progress {
  position: relative;
  width: 100%;
  height: 4px;
  border-radius: var(--r-full);
  background: var(--surface-3);
  overflow: hidden;
}

.progress.lg { height: 8px; }

.progress-bar {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: var(--accent);
  transition: width var(--dur-3) var(--ease);
}

.progress-bar.good { background: var(--status-good); }
.progress-bar.bad  { background: var(--status-critical); }

/* --- stage list (ingest / classification pipeline) ------------------------ */

.stage-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
}

.stage {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--line-faint);
  font-family: var(--font-ui);
  font-size: var(--fs-tiny);
  color: var(--text-muted);
  min-width: 0;
}

.stage:last-child { border-bottom: 0; }

.stage::before {
  content: "";
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: var(--r-full);
  border: 1px solid var(--line-strong);
  background: transparent;
}

.stage-name {
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stage-msg {
  margin-left: auto;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stage.on {
  color: var(--text-primary);
}

.stage.on::before {
  border-color: var(--accent);
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-wash);
  animation: ep-pulse 1.4s var(--ease) infinite;
}

.stage.done {
  color: var(--text-secondary);
}

.stage.done::before {
  border-color: var(--status-good);
  background: var(--status-good);
}

.stage.done .stage-msg::after {
  content: " \2713";
  color: var(--status-good);
}

@keyframes ep-pulse {
  0%   { box-shadow: 0 0 0 0 var(--accent-glow); }
  70%  { box-shadow: 0 0 0 6px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* ==========================================================================
   9. SOCIAL — feed, posts, comments, profile
   ========================================================================== */

.feed {
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
  min-width: 0;
}

.post {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  padding: var(--s4);
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  min-width: 0;
}

.post-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-width: 0;
}

.post-head .avatar { --avatar-size: 40px; }

.post-who {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.post-name {
  font-size: var(--fs-base);
  font-weight: 500;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.post-handle,
.post-time {
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  letter-spacing: 0;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.post-time { margin-left: auto; align-self: flex-start; }

.post-body {
  font-size: var(--fs-base);
  line-height: 1.6;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
  min-width: 0;
}

.post-body strong { color: var(--text-primary); font-weight: 600; }

.post-media {
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding: var(--s3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  min-width: 0;
  overflow: hidden;
}

.post-media .fingerprint-chip {
  width: 96px;
  height: 96px;
  border-radius: var(--r-md);
  background: transparent;
  border-color: var(--line-faint);
}

.post-media .thumbstrip { flex: 1 1 auto; padding-bottom: 0; }

.post-media-meta {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  min-width: 0;
  flex: 1 1 auto;
}

.post-actions {
  display: flex;
  align-items: center;
  gap: var(--s5);
  padding-top: var(--s2);
  border-top: 1px solid var(--line-faint);
}

.post-action {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  transition: color var(--dur-1) var(--ease);
}

.post-action:hover { color: var(--text-primary); }

.post-action.on { color: var(--accent-text); }

.post-action svg { width: 14px; height: 14px; }

.comment {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s3) 0;
  border-top: 1px solid var(--line-faint);
  min-width: 0;
}

.comment .avatar { --avatar-size: 24px; }

.comment-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--fs-small);
  line-height: 1.55;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.comment-body .post-handle { display: inline; }

.follow-btn {
  flex: 0 0 auto;
  min-width: 88px;
}

.follow-btn.on {
  background: var(--surface-1);
  border-color: var(--line-control);
  color: var(--text-secondary);
}

.follow-btn.on:hover {
  border-color: var(--status-critical);
  color: var(--status-critical);
  background: var(--status-critical-wash);
}

/* --- profile -------------------------------------------------------------- */

.profile-head {
  position: relative;
  margin-bottom: var(--s6);
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
}

/* A plain grey band under a hairline. The old tinted bloom read as decoration,
   and decoration is what this direction spends nothing on. */
.profile-cover {
  height: 96px;
  border-bottom: 1px solid var(--line);
  background-color: var(--surface-2);
  background-image: none;
}

/* Only the avatar breaks the cover hairline. Pulling the whole row up (as the
   old rule did) floated the name and handle onto the cover band, which the
   tinted gradient used to hide and a flat grey band does not. */
.profile-id {
  display: flex;
  align-items: flex-start;
  gap: var(--s4);
  padding: var(--s4) var(--s5);
  margin-top: 0;
  min-width: 0;
}

.profile-id .avatar {
  --avatar-size: 88px;
  --avatar-ring: var(--line-strong);
  margin-top: -44px;
  background: var(--surface-1);
  box-shadow: inset 0 0 0 1px var(--avatar-ring), 0 0 0 4px var(--surface-1);
}

.profile-meta {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  min-width: 0;
  padding-bottom: var(--s1);
}

.profile-actions { margin-left: auto; padding-bottom: var(--s1); }

.profile-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
  padding: var(--s3) var(--s5);
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}

@media (max-width: 640px) {
  .profile-id {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s3);
    padding: var(--s4);
  }
  .profile-id .avatar { --avatar-size: 68px; margin-top: -38px; }
  .profile-actions { margin-left: 0; }
  .profile-stats { gap: var(--s4); padding: var(--s3) var(--s4); }
  .post-media { flex-direction: column; align-items: stretch; }
  .post-media .fingerprint-chip { width: 100%; height: 140px; }
}

/* ---------------------------------------------------------------------------
   A chart that lives inside a card.

   Views wrap each chart in a card so the section can carry its own heading and
   framing copy. Left alone that renders two nested borders and, where the card
   names the same thing the chart does, the title twice. So: a nested chart goes
   flush and lets the card own the frame, and it drops its own title only when
   the card actually has a head to carry one — a card with no head still needs
   the chart's title to say what the reader is looking at.
   ------------------------------------------------------------------------ */
.card .chart {
  border: 0;
  border-radius: 0;
  background: transparent;
}
.card .chart > .chart-head {
  padding-left: 0;
  padding-right: 0;
}
.card:has(> .card-head) .chart > .chart-head > .chart-title { display: none; }
.card:has(> .card-head) .chart > .chart-head > .chart-sub { padding-left: 0; }

/* ---------------------------------------------------------------------------
   Narrow widths: nothing is allowed to push the page sideways (CONTRACT §8).
   A head row wraps rather than overflowing, and anything genuinely wider than
   the phone — a dense table, a wide control group — scrolls inside its own
   container.
   ------------------------------------------------------------------------ */
@media (max-width: 760px) {
  .card-head,
  .chart-head,
  .page-head,
  .page-actions { flex-wrap: wrap; row-gap: var(--s2); }

  .card-body { overflow-x: auto; }

  .segmented,
  .tabs {
    max-width: 100%;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .segmented::-webkit-scrollbar,
  .tabs::-webkit-scrollbar { display: none; }
  .segmented > * { flex: 0 0 auto; }
}

/* ---------------------------------------------------------------------------
   Admin feedback capture (EPL-444/EPL-454) — components/feedback.js.
   Distinct from CONTRACT §9 "Feedback" (toast/modal, above): this is the
   board's own bug-report tool, not user-facing confirmation.
   ------------------------------------------------------------------------ */

.fb-btn[aria-pressed="true"] {
  color: var(--accent-text);
  background: var(--accent-wash);
}

/* The button's twin while a dialog is open (EPL-2552): fixed over the topbar
   button, which feedback.js measures and writes as left/top, and above the
   dialog's backdrop (.modal-host, 100), below the toasts (120) and the pick
   marks (200). The corner is only for a topbar button that has no box.
   base.css lifts any focused control to `position: relative; z-index: 3`,
   which would drop the twin back under the backdrop after a key press, hence
   the :focus-visible line. */
.fb-btn-twin,
.fb-btn-twin:focus-visible {
  position: fixed;
  z-index: 110;
}

.fb-btn-twin {
  top: var(--s3);
  right: var(--s3);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
}

.fb-btn-twin:focus-visible { box-shadow: var(--ring), var(--shadow-2); }

body.fb-picking,
body.fb-picking * { cursor: crosshair !important; }

body.fb-picking { user-select: none; }

.fb-pick-highlight,
.fb-pick-box {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 200;
  pointer-events: none;
  border-radius: var(--r-sm);
}

.fb-pick-highlight {
  border: 2px solid var(--accent);
  background: var(--accent-glow);
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease);
}

.fb-pick-box {
  border: 2px dashed var(--accent);
  background: var(--accent-glow);
}

.fb-pick-banner {
  position: fixed;
  top: var(--s4);
  left: 50%;
  transform: translateX(-50%);
  z-index: 201;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--shadow-3);
  color: var(--text-primary);
  font-size: var(--fs-base);
  cursor: default;
}

.fb-preview {
  display: block;
  width: 100%;
  max-height: 320px;
  object-fit: contain;
  object-position: top left;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

@media (prefers-reduced-motion: reduce) {
  .fb-pick-highlight { transition: none; }
}

/* ==========================================================================
   10. RUNTIME-SCOPED COMPONENT CSS
   ----------------------------------------------------------------------------
   Until EPL-544, each module below injected this block itself at import, via
   `document.createElement('style')` — a runtime <style> *element*, which is
   the style-src-elem CSP sub-resource (distinct from style-src-attr, which
   EPL-472 closed). Moving it here drops 'unsafe-inline' from style-src-elem
   too: components.css is a static stylesheet, loaded once from index.html.
   Ownership is unchanged — each block below is read only by the module named
   in its header, and only that module renders the markup it selects.
   ========================================================================== */

/* -- web/js/components/table.js ---------------------------------------------- */
.ep-dt { position: relative; display: block; min-width: 0; }
.ep-dt-scroll {
  position: relative;
  overflow: auto;
  max-width: 100%;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.ep-dt-scroll > table { min-width: 100%; }
.ep-dt table thead th.ep-dt-static { position: static; }
.ep-dt th.th-sort { outline: none; }
.ep-dt th.th-sort:focus-visible { box-shadow: var(--ring); position: relative; z-index: 3; }
.ep-dt tbody tr.ep-dt-row { outline: none; }
.ep-dt[data-clickable="1"] tbody tr.ep-dt-row { cursor: pointer; }
.ep-dt tbody tr.ep-dt-row.on td { background: var(--accent-wash); }
.ep-dt .ep-dt-nowrap { white-space: nowrap; }
.ep-dt-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border-top: 1px solid var(--line-faint);
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--text-muted);
}

/* -- web/js/components/imagegrid.js ------------------------------------------ */
.ep-igrid { display: block; min-width: 0; }
.ep-imgrid { outline: none; }
.ep-imgrid .imgcell img {
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease);
}
.ep-imgrid .imgcell img[data-loaded="1"] { opacity: 1; }
.ep-imgrid[data-cv="1"] > .imgcell {
  content-visibility: auto;
  contain-intrinsic-size: auto var(--cell, 104px);
}
.ep-imgrid .imgcell > .conf-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  width: auto;
  height: 3px;
  border-radius: 0;
  z-index: 2;
}
.ep-imgrid .imgcell.has-conf .imgcell-meta { bottom: 3px; }
.ep-imgrid .ep-ic-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ep-imgrid .ep-ic-label.confirmed { color: var(--text-primary); }
.ep-imgrid .ep-ic-label.predicted { color: var(--text-secondary); }
.ep-imgrid .ep-ic-check { margin-right: 3px; }
@media (prefers-reduced-motion: reduce) {
  .ep-imgrid .imgcell img { transition: none; }
}
/* To scale: the full picture at its own pixel size times --img-px (CSS px per
   image pixel), centred, never stretched. Until it has loaded its size is 0,
   so nothing is drawn at a wrong size first. */
.ep-imgrid.to-scale .imgcell { background: var(--specimen-bg); }
.ep-imgrid.to-scale .imgcell img {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--nat-w, 0) * var(--img-px, 1) * 1px);
  height: calc(var(--nat-h, 0) * var(--img-px, 1) * 1px);
  max-width: none;
  object-fit: fill;
  transform: translate(-50%, -50%);
}
/* Larger than its tile at this zoom: cropped, and the tile says so. */
.ep-imgrid.to-scale .imgcell.over { box-shadow: inset 0 0 0 1px var(--line-strong); }
.imgcell-scale {
  position: absolute;
  right: 4px;
  top: 4px;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  padding: 2px 4px;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--bg-deep) 70%, transparent);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.imgcell-scale > i { display: block; width: var(--bar-px, 0); height: 2px; background: currentColor; }

/* -- web/js/components/sortcontrol.js ---------------------------------------- */
.sortc { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); min-width: 0; }
.sortc .select { flex: 0 1 auto; width: auto; min-width: 0; }
.sortc-shortcuts { display: contents; }
/* Both directions read the same width, so a reversal moves nothing beside it. */
.sortc-dir { min-width: 118px; justify-content: center; }
.sortc-about {
  margin: 0;
  font-size: var(--fs-tiny);
  line-height: 1.45;
  color: var(--text-muted);
}

/* -- web/js/components/scalecontrol.js --------------------------------------- */
.scalec { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--s2); min-width: 0; }
.scalec .chip[disabled] { opacity: 0.5; cursor: not-allowed; }
.scalec .scalec-zoom { flex: 0 1 auto; width: auto; min-width: 0; }

/* -- web/js/components/rangefilter.js ---------------------------------------- */
.rangef { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.rangef-edit { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); min-width: 0; }
.rangef-edit .select { flex: 0 1 auto; width: auto; min-width: 0; }
.rangef-edit .input.num { width: 84px; }
.rangef-unit { min-width: 2.5em; color: var(--text-muted); font-size: var(--fs-small); }
/* One line whatever is set: a longer list is clipped and kept whole in the hover. */
.rangef-line { display: flex; align-items: center; gap: var(--s2); min-width: 0; min-height: 28px; }
.rangef-summary {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-tiny);
  color: var(--text-secondary);
}

/* -- web/js/components/specimen.js ------------------------------------------- */
.specimen-px { color: var(--text-muted); white-space: nowrap; }
/* The picture keeps its own size; the bar sits on it, bottom right. */
.specimen-figure { position: relative; display: inline-block; margin: 0; max-width: 100%; line-height: 0; }
.specimen-bar {
  position: absolute;
  right: 6px;
  bottom: 6px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  padding: 3px 5px;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--bg-deep) 72%, transparent);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.specimen-bar[hidden] { display: none; }
.specimen-bar > i { display: block; height: 2px; background: currentColor; }

/* -- web/js/components/taxonpicker.js ---------------------------------------- */
.ep-tp { position: relative; display: block; min-width: 0; }
.ep-tp .search { max-width: none; flex: 1 1 auto; width: 100%; }
.ep-tp .search .input { padding-right: 30px; width: 100%; }
.ep-tp-clear {
  position: absolute;
  z-index: 4;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  display: none;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  line-height: 1;
  cursor: pointer;
}
.ep-tp[data-filled="1"] .ep-tp-clear { display: block; }
.ep-tp-clear:hover { color: var(--text-primary); background: var(--surface-3); }
/* base.css's `button:focus-visible` (0,1,1) sets position: relative, which
   would drop the focused ✕ out of the field into the flex row. */
.ep-tp-clear:focus-visible { position: absolute; z-index: 4; box-shadow: var(--ring); }
.ep-tp-current {
  margin-top: var(--s1);
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  line-height: 1.4;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ep-tp-list {
  position: absolute;
  z-index: 60;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  max-height: 296px;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s1) 0;
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-2);
}
.ep-tp-list[hidden] { display: none; }
.ep-tp-group-label {
  padding: var(--s2) var(--s3) var(--s1);
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--text-muted);
}
.ep-tp-opt {
  display: block;
  padding: var(--s2) var(--s3);
  cursor: pointer;
  border-left: 2px solid transparent;
}
.ep-tp-opt:hover { background: var(--surface-3); }
.ep-tp-opt[aria-selected="true"] {
  background: var(--accent-wash);
  border-left-color: var(--accent);
}
.ep-tp-name {
  display: block;
  font-size: var(--fs-small);
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ep-tp-path {
  display: block;
  margin-top: 1px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ep-tp-hit {
  font-weight: 700;
  color: var(--text-primary);
  background: var(--accent-wash);
  border-radius: 2px;
}
.ep-tp-msg {
  padding: var(--s3);
  font-family: var(--font-ui);
  font-size: var(--fs-tiny);
  line-height: 1.5;
  color: var(--text-muted);
}

/* --- rendered Markdown (components/markdown.js) ---------------------------
   The team's notes on the admin pages. base.css strips lists and margins
   everywhere; inside `.md` they come back, at the reading face and measure. */

.md {
  max-width: 78ch;
  font-family: var(--font-body);
  font-size: var(--fs-small);
  line-height: 1.6;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.md > * + * { margin-top: var(--s3); }
.md li > * + * { margin-top: var(--s2); }

.md h3, .md h4, .md h5, .md h6 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.3;
  color: var(--text-primary);
}

.md h3 { font-size: var(--fs-lg); margin-top: var(--s6); }
.md h4 { font-size: var(--fs-base); margin-top: var(--s5); }
.md h5, .md h6 { font-size: var(--fs-small); margin-top: var(--s4); }
.md > :first-child { margin-top: 0; }

.md strong { font-weight: 600; color: var(--text-primary); }
.md code { font-size: .9em; padding: 0 3px; border-radius: var(--r-sm); background: var(--surface-2); }
.md pre { padding: var(--s3); border-radius: var(--r-md); background: var(--surface-sunken); overflow-x: auto; }
.md pre code { padding: 0; background: none; }

.md ul, .md ol { padding-left: var(--s5); }
.md ul { list-style: disc; }
.md ol { list-style: decimal; }
.md ul ul { list-style: circle; }
.md li + li { margin-top: var(--s1); }

.md blockquote {
  padding-left: var(--s3);
  border-left: 2px solid var(--line-strong);
  color: var(--text-muted);
}

.md .md-table { overflow-x: auto; }
.md .md-table td, .md .md-table th { vertical-align: top; }

/* --- the skill score (components/skill.js, EPL-2680) ----------------------- */

.sk { min-width: 0; }
.sk-lead, .sk-none { font-size: var(--fs-small); color: var(--text-secondary); max-width: 72ch; }
.sk-note { font-size: var(--fs-micro); line-height: 1.5; max-width: 80ch; }
.sk-scroll { overflow-x: auto; min-width: 0; }
.sk-caption {
  caption-side: top;
  text-align: left;
  padding-bottom: var(--s2);
  font-size: var(--fs-micro);
  color: var(--text-muted);
}
.table.sk-matrix { width: auto; }
.sk .table tbody th {
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--line-faint);
  font-weight: 400;
  text-align: left;
}
.sk .table tbody tr:last-child th { border-bottom: 0; }
.sk-class { color: var(--text-primary); white-space: nowrap; }
.sk-rate { min-width: 14ch; }
.sk-rate.dim { font-size: var(--fs-tiny); }
.sk-shared .sk-why { display: block; font-size: var(--fs-micro); }
.sk-corner span { display: block; }
.sk-colname {
  display: inline-block;
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: bottom;
}
.sk-matrix .sk-cell { min-width: 5ch; }
.sk-matrix .sk-right { background: var(--accent-wash); color: var(--text-primary); }
.sk-matrix .sk-zero { color: var(--text-muted); }
.sk-matrix .sk-unsure,
.sk-matrix tr > td:last-child { border-left: 1px solid var(--line); }
.sk-member > summary { cursor: pointer; align-items: center; padding: var(--s2) 0; }
.sk-member > .sk { padding: var(--s2) 0 var(--s4); }
.sk-member + .sk-member { border-top: 1px solid var(--line-faint); }

/* ---------------------------------------------------------------------------
   "What's in this sample" — components/relative-abundance.js (#1028)
   One row per taxon: name, bar with its 95 % whisker, share and range, n,
   confidence dot, checked tick. The bar takes its group's colour
   (engine.groupColor, as on the Explore taxa table) through --ra-color.
   ------------------------------------------------------------------------- */
.ra-search { position: relative; max-width: 520px; }
.ra-suggest {
  position: absolute;
  z-index: 60;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  max-height: 320px;
  overflow-y: auto;
  margin: 0;
  padding: var(--s1) 0;
  list-style: none;
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-2);
}
.ra-suggest[hidden] { display: none; }
.ra-opt {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  cursor: pointer;
  border-left: 2px solid transparent;
}
.ra-opt:hover { background: var(--surface-3); }
.ra-opt[aria-selected="true"] { background: var(--accent-wash); border-left-color: var(--accent); }
.ra-opt-name { color: var(--text-primary); }
.ra-opt-meta { margin-left: auto; font-size: var(--fs-tiny); white-space: nowrap; }
.ra-suggest-msg { padding: var(--s2) var(--s3); color: var(--text-muted); font-size: var(--fs-small); }
.ra-answer {
  margin: 0;
  padding: var(--s3) var(--s4);
  max-width: 96ch;
  border-left: 2px solid var(--accent);
  background: var(--surface-2);
  border-radius: var(--r-sm);
  font-size: var(--fs-base);
}
.ra-answer[hidden] { display: none; }
.ra-answer-link { color: var(--text-primary); text-decoration: underline; text-underline-offset: 3px; }
.ra-answer-link:hover { color: var(--accent); }
.ra-list { max-height: 640px; overflow-y: auto; border-top: 1px solid var(--line); }
.ra-list[hidden] { display: none; }
.ra-axis {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: var(--s2) 0;
  background: var(--surface-1);
  border-bottom: 1px solid var(--line-faint);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
}
.ra-axis-title { grid-column: 1; }
.ra-ticks { grid-column: 2; display: flex; justify-content: space-between; text-transform: none; letter-spacing: 0; }
.ra-group, .ra-pinned { border-bottom: 1px solid var(--line-faint); }
.ra-group-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) 0;
  cursor: pointer;
}
.ra-pinned .ra-group-head { cursor: default; }
.ra-group-name { font-weight: 600; color: var(--text-primary); }
.ra-group-total { font-size: var(--fs-small); }
.ra-rows { margin: 0 0 var(--s2); padding: 0; list-style: none; }
.ra-row {
  display: grid;
  grid-template-columns: minmax(10rem, 16rem) minmax(8rem, 1fr) 12ch 6ch 14px 14px;
  align-items: center;
  gap: var(--s3);
  padding: 3px 0;
}
.ra-row:not(.ra-axis):hover { background: var(--surface-hover); }
.ra-name {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-primary);
  font: inherit;
  font-size: var(--fs-small);
  text-align: left;
  cursor: pointer;
}
.ra-name:hover .truncate { text-decoration: underline; }
.ra-name:focus-visible { box-shadow: var(--ring); border-radius: var(--r-sm); }
.ra-harmful { flex: 0 0 auto; }
.ra-track { position: relative; height: 10px; background: var(--surface-3); border-radius: 2px; }
.ra-bar {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  min-width: 1px;
  border-radius: 2px;
  background: var(--ra-color, var(--series-other));
}
.ra-bar.hatched {
  background-image: repeating-linear-gradient(135deg, var(--surface-1) 0 1.2px, transparent 1.2px 3px);
}
.ra-whisker {
  position: absolute;
  top: 50%;
  height: 0;
  border-top: 1px solid var(--text-primary);
}
.ra-whisker::before,
.ra-whisker::after {
  content: "";
  position: absolute;
  top: -4px;
  height: 7px;
  border-left: 1px solid var(--text-primary);
}
.ra-whisker::before { left: 0; }
.ra-whisker::after { right: 0; }
.ra-figure { display: flex; flex-direction: column; font-size: var(--fs-small); line-height: 1.2; }
.ra-range { font-size: var(--fs-micro); color: var(--text-muted); }
.ra-outside { color: var(--text-muted); font-size: var(--fs-small); }
.ra-n { font-size: var(--fs-small); text-align: right; color: var(--text-secondary); }
.ra-conf {
  width: 9px;
  height: 9px;
  border-radius: var(--r-full);
  border: 1px solid var(--seq-500);
}
.ra-conf[data-band="low"]  { background: var(--seq-300); border-color: var(--seq-300); }
.ra-conf[data-band="mid"]  { background: var(--seq-400); border-color: var(--seq-400); }
.ra-conf[data-band="high"] { background: var(--seq-500); }
.ra-conf[data-band="none"] { border-style: dashed; border-color: var(--text-muted); }
.ra-tick { color: var(--status-good); font-size: var(--fs-small); text-align: center; }
.ra-table { max-height: 640px; overflow: auto; }
.ra-table[hidden] { display: none; }
.ra-foot { margin: 0; font-size: var(--fs-micro); max-width: 96ch; }
