/* UAMH LIMS — design system, aligned to the UAMH brand guide (navy + teal,
   flat, light-first, with a dark variant).
   DESKTOP-FIRST: ~99% of use is at a desk. Base rules target a large screen
   with dense, scannable data. Narrow-screen rules are additive fallbacks. */

:root {
  /* ---- UAMH brand tokens (from the guide) ---- */
  --uamh-navy:        #1C3866;
  --uamh-navy-hover:  #16294C;
  --uamh-teal:        #017FA3;
  --uamh-teal-hover:  #016783;
  --uamh-teal-active: #01536B;
  --uamh-teal-tint:   #E8F2F6;
  --uamh-coral:       #F0533F;
  --uamh-coral-hover: #D93F2C;
  --uamh-callout:     #CADFE8;

  /* ---- generic aliases the app is built on ---- */
  --bg:        #ffffff;   /* white page, per the guide */
  --surface:   #ffffff;   /* cards, inputs */
  --surface-2: #F6F6F7;   /* utility bar, thead, zebra, label cells, insets */
  --border:    #D0D1C9;   /* cell / input borders */
  --border-2:  #E7E8E2;   /* light row dividers */
  --rule:      #C7C7C9;   /* section rules */
  /* The app bar, as a token rather than a literal, because it is navy on light
     and a lifted near-black on dark. A token keeps that difference in the two
     theme blocks with every other colour that changes, instead of adding a
     third place where a dark rule can be written once and forgotten. */
  --topbar-bg: var(--uamh-navy);

  --text:      #1A2331;
  --text-dim:  #526388;
  --heading:   var(--uamh-navy);   /* headings, thead, labels, navy chrome text */

  --accent:    var(--uamh-teal);        /* "you can click this" */
  --accent-dk: var(--uamh-teal-hover);
  --accent-bg: var(--uamh-teal-tint);   /* row hover, selected, teal badge */

  --danger:    #B03A28;  --danger-bg: #F7E7E4;   /* status: contaminated, RG3 */
  --warn:      #8A5A00;  --warn-bg:   #F6EFDD;   /* status: low, RG2, review */
  --success:   #1F7A55;  --success-bg:#E4F1EA;   /* status: viable, passed QC */
  --info-bg:   var(--uamh-callout);

  --font-sans: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, "Segoe UI", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Roboto Mono", monospace;

  --radius-sm: 3px;   /* inputs, badges */
  --radius:    6px;   /* buttons, cards, callouts */
  --radius-lg: 10px;  /* modals */
  --row:       1.9rem;
}

/* Dark variant — off-spec (the guide is light-only) but kept UAMH-aligned:
   navy chrome, teal accent, brand hues carried through. */
/* ======================================================================
   THEME: AUTO, LIGHT OR DARK

   Dark is reached two ways, so every dark rule below is written for both:

     @media (prefers-color-scheme: dark), on :root:not([data-theme="light"])
         the reader has chosen Auto -- or has no JavaScript and so never got an
         attribute at all -- and the operating system decides.
     :root[data-theme="dark"]
         the reader has chosen Dark outright, whatever the OS says.

   Each pair is generated from ONE body and MUST be changed together. There is
   no way to express "this media query, or that attribute" as a single CSS
   condition, and the alternative -- resolving the theme in JavaScript and
   emitting only an attribute -- would leave a reader with scripting disabled
   on a permanently light screen.

   THE ADDED ANCESTOR IS WRAPPED IN :where() SO IT COSTS NO SPECIFICITY.
   Without that, `:root[data-theme="dark"] .chart-dot` would score (0,3,0) and
   outrank `.sheet .chart-dot` at (0,2,0) -- and the printed sheet would begin
   following the screen's colour scheme, which is the one thing it must never
   do. :where() leaves every selector exactly as specific as it is today.
   ====================================================================== */
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    --bg:#0F1622; --surface:#161F2E; --surface-2:#1B2637;
    --topbar-bg:#191F2D;
    --border:#2C3850; --border-2:#243049; --rule:#2C3850;
    --text:#E7ECF3; --text-dim:#9FB0CC; --heading:#CDDDF2;
    --accent:#3AA9C9; --accent-dk:#63C3DD; --accent-bg:#12303C;
    --danger:#E88B7C; --danger-bg:#2C1A17; --warn:#E0B458; --warn-bg:#2A2314;
    --success:#63C399; --success-bg:#14261D;
    --uamh-callout:#17303F; --info-bg:#17303F;
  }
}
/* @media screen, because Chrome forces prefers-color-scheme: light when it
   prints. That makes the media-query copy above inert on paper for free; an
   attribute selector keeps matching, so without this a reader who chose Dark
   would print --heading #CDDDF2 on white paper and lose every heading. The
   two forms of dark must behave the same in EVERY context, not just on a
   screen. */
@media screen {
  :root:where([data-theme="dark"]) {
      --bg:#0F1622; --surface:#161F2E; --surface-2:#1B2637;
      --topbar-bg:#191F2D;
      --border:#2C3850; --border-2:#243049; --rule:#2C3850;
      --text:#E7ECF3; --text-dim:#9FB0CC; --heading:#CDDDF2;
      --accent:#3AA9C9; --accent-dk:#63C3DD; --accent-bg:#12303C;
      --danger:#E88B7C; --danger-bg:#2C1A17; --warn:#E0B458; --warn-bg:#2A2314;
      --success:#63C399; --success-bg:#14261D;
      --uamh-callout:#17303F; --info-bg:#17303F;
  }
}


/* Tell the browser which scheme is in force, so form controls, scrollbars,
   the caret and the default focus ring follow the choice too. Auto is left
   to the <meta name="color-scheme"> tag in the layout. */
@media screen {
  :root:where([data-theme="light"]) { color-scheme: light; }
  :root:where([data-theme="dark"])  { color-scheme: dark; }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 13.5px/1.45 var(--font-sans);
}
a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; text-decoration-thickness: 1px; }
a:hover { color: var(--accent-dk); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- app bar: one navy bar (brand · nav · user) with a 4px teal rule ---- */
.topbar {
  display: flex; align-items: center; gap: 1rem;
  padding: 0 1.1rem; min-height: 52px;
  background: var(--topbar-bg); color: #fff;
  /* On dark the bar sits only just above the page — 1.10:1, which is a tone,
     not an edge. This teal rule is what actually separates them. It was
     decoration when the bar was navy; it is the edge of the header on dark.
     Do not drop it. */
  border-bottom: 4px solid var(--uamh-teal);
}
.topbar a { color: #fff; text-decoration: none; }
.topbar .brand { display: inline-flex; align-items: center; gap: .5rem; flex: 0 0 auto; }
.topbar .brand .mark   { height: 34px; width: auto; display: block; }
.topbar .brand .lockup { height: 24px; width: auto; display: block; }
/* THE WORDMARK IS THE FIRST THING TO GO, and it stays gone far longer than it
   used to. Measured intrinsic widths of the whole bar: 1336px with the wordmark
   and an inline nav, 1150px without it, 1054px without it and with the name
   fully truncated. The old band gave the wordmark back at 1001px — nearly 340px
   before the bar could afford it — so every width from there to about 1340
   pushed the page into a horizontal scroll. It now returns only above 1340px.
   Below 1100 the nav is behind the hamburger and the bar needs 310px in total,
   so the wordmark is affordable again and shows. */
@media (min-width: 1101px) and (max-width: 1340px) { .topbar .brand .lockup { display: none; } }
.nav { display: flex; gap: .1rem; margin-right: auto; }
.nav a {
  padding: .9rem .7rem; color: rgba(255,255,255,.82); text-decoration: none;
  font-size: .9rem; border-bottom: 3px solid transparent;
}
.nav a:hover { color: #fff; }
.nav a[aria-current="page"] { color: #fff; font-weight: 600; border-bottom-color: var(--uamh-teal); }
.topbar-user { display: flex; align-items: center; gap: .6rem; min-width: 0; }
/* THE ONLY PART OF THIS BAR WHOSE WIDTH DEPENDS ON WHO IS LOOKING AT IT.
   Everything else here — nine nav labels, three theme buttons, a role pill, a
   Sign out — is the same on every screen and can be measured once. A display
   name cannot, so it is bounded instead: it shrinks first and ends in an
   ellipsis rather than pushing the bar wider than the window. Without this the
   width at which the header fits is a property of the user table, and the
   breakpoints below would be right for the account they were measured against
   and wrong for the next person to sign in. (min-width: 0 on both this and its
   parent: a flex item's default floor is its own min-content, so the ellipsis
   never gets a chance without it.) */
.topbar .who  { font-size: .82rem; opacity: .9;
                min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar .role {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .06em;
  background: rgba(255,255,255,.16); padding: .1rem .4rem; border-radius: 999px;
}

.topbar form { margin: 0; }
.topbar button {
  background: transparent; color: #fff; border: 1px solid rgba(255,255,255,.4);
  border-radius: var(--radius); padding: .2rem .6rem; font: inherit; font-size: .8rem; cursor: pointer;
}
.topbar button:hover { background: rgba(255,255,255,.12); }

/* AFTER `.topbar button`, NOT BEFORE IT. Both `.themetoggle button` and
   `.topbar button` score (0,1,1), and both `.themetoggle button[aria-pressed]`
   and `.topbar button:hover` score (0,2,1) — equal specificity on each pair, so
   SOURCE ORDER decides and the later rule wins. Written above the generic bar
   button, almost every declaration here was dead: the segments rendered as three
   separately outlined boxes, and hovering the SELECTED one let
   `.topbar button:hover` strip its white background, dropping the only sighted
   indication of which theme is active to 1.43:1. Do not move this block back
   up the file. */
/* Colour-theme switch, in the bar beside the reader's name. Three states and
   not two, because "Auto" is a real choice and not the absence of one: it says
   follow this machine, which is what most people want and what someone who
   works late actually notices. A two-state toggle cannot express it -- once you
   have touched it you are pinned to a scheme for ever.

   Segmented rather than a cycling button: a cycle hides where you are until you
   press it, and there is no way to jump straight from Dark back to Auto.

   Sized off the bar's own chrome (the same translucent white as .role and the
   Sign out button) so it belongs to the bar in either scheme. The bar is navy
   on light and #191F2D on dark, so this control still does NOT take part in the
   theme it sets: every value here is white at some alpha, which composites
   against whichever of the two the bar is wearing and stays legible on both.
   That is why the chrome survived the bar going dark unchanged -- it was never
   pinned to navy, only to "lighter than the bar". */
.themetoggle {
  display: inline-flex; align-items: center; gap: 1px;
  background: rgba(255,255,255,.12); border-radius: 999px; padding: 2px;
}
.themetoggle button {
  border: 0; background: transparent; color: rgba(255,255,255,.72);
  border-radius: 999px; padding: .2rem .42rem; cursor: pointer;
  font: inherit; font-size: .72rem; line-height: 1;
  display: inline-flex; align-items: center;
}
.themetoggle button:hover { background: rgba(255,255,255,.14); color: #fff; }
.themetoggle button[aria-pressed="true"] {
  background: rgba(255,255,255,.92); color: var(--uamh-navy);
}
.themetoggle svg { width: 14px; height: 14px; display: block; fill: none;
                   stroke: currentColor; stroke-width: 1.7;
                   stroke-linecap: round; stroke-linejoin: round; }
/* Nothing to switch on paper, and the bar itself is already hidden there. */
@media print { .themetoggle { display: none; } }

/* A checkbox and its explanation on one line, so the box is not stranded under
   a label with nothing beside it. The label element wraps both, which makes the
   text a click target for the box as well. */
.checkline { display: inline-flex; align-items: center; gap: .45rem; font-weight: 400; }
.checkline input { margin: 0; }
.checkline span  { font-size: .82rem; color: var(--text-dim); }

/* Hamburger: hidden on desktop; the menu wrapper is transparent to layout so
   nav + user flow as flex siblings exactly as before. */
.navtoggle { display: none; }
.topbar-menu { display: contents; }

/* ---- collapsed header: nav behind a hamburger --------------------------
   AT 1100px, NOT 700. The inline nav is nine labels and needs 726px on its
   own; with the brand, the theme switch, the role pill and Sign out around it
   the bar cannot be drawn in less than 1054px however hard everything in it
   is squeezed. It used to keep trying anyway down to 700px, and what it did
   with the widths in between was push the whole PAGE sideways — a horizontal
   scrollbar on every screen from a tablet to a small laptop, on every page in
   the app, because this header is in the layout.

   1100px is not a fresh number: it is the width where .grid.cols-2 already
   collapses and the dashboard's map already reflows. The app had one boundary
   between its wide and narrow layouts and a header that disagreed with it by
   400px. Now they agree. The drawer needs 310px in total, so it holds from
   here down to the narrowest phone. */
@media (max-width: 1100px) {
  .topbar { position: relative; }
  .navtoggle {
    display: inline-flex; align-items: center; justify-content: center;
    margin-left: auto; width: 2.4rem; height: 2.4rem; padding: 0;
    background: transparent; border: 1px solid rgba(255,255,255,.4);
    border-radius: var(--radius); cursor: pointer;
  }
  .navtoggle .bars, .navtoggle .bars::before, .navtoggle .bars::after {
    content: ""; display: block; width: 18px; height: 2px; background: #fff; border-radius: 2px;
  }
  .navtoggle .bars { position: relative; }
  .navtoggle .bars::before { position: absolute; top: -6px; left: 0; }
  .navtoggle .bars::after  { position: absolute; top:  6px; left: 0; }

  .topbar-menu {
    display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 40;
    flex-direction: column; align-items: stretch;
    background: var(--topbar-bg); border-bottom: 4px solid var(--uamh-teal);
    /* The drawer follows the bar, or a narrow window would show a navy panel
    hanging off a near-black header. */
    padding: .3rem 0 .5rem; box-shadow: 0 8px 20px rgba(28,56,102,.28);
  }
  .topbar-menu.open { display: flex; }
  .nav { flex-direction: column; margin: 0; }
  .nav a {
    padding: .6rem 1.1rem; font-size: 1rem; border-bottom: none;
    border-left: 3px solid transparent;
  }
  .nav a[aria-current="page"] { border-bottom: none; border-left-color: var(--uamh-teal); }
  .topbar-user {
    justify-content: flex-start; gap: .7rem; padding: .55rem 1.1rem 0; margin-top: .35rem;
    border-top: 1px solid rgba(255,255,255,.16);
  }
  .topbar-user .who { margin-right: auto; }
}

/* ---- layout: use the width ------------------------------------------- */
/* The topbar has no bottom margin of its own — only a 4px teal rule — so this
   top padding is the whole gap between the navigation and the first panel. */
main { padding: 1.6rem 1.1rem 3rem; max-width: 1700px; margin: 0 auto; }
h1 { font-size: 1.55rem; line-height: 1.2; margin: .2rem 0 .8rem; font-weight: 700; color: var(--heading); }
h2 { font-size: 1.12rem; margin: 1.3rem 0 .5rem; font-weight: 700; color: var(--heading); }
h3 { font-size: 1rem; font-weight: 700; color: var(--heading); }
h2 .count { color: var(--text-dim); font-weight: 400; font-size: .8em; }

.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .8rem .9rem; margin-bottom: .8rem;
}
.card.tight { padding: 0; overflow: hidden; }
.card > h2:first-child { margin-top: 0; }

.grid { display: grid; gap: .8rem; }
.grid.cols-2 { grid-template-columns: 1fr 1fr; }
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid.sidebar { grid-template-columns: minmax(0,1fr) 22rem; align-items: start; }
@media (max-width: 1100px) {
  .grid.cols-2, .grid.cols-3, .grid.sidebar { grid-template-columns: 1fr; }
}

/* ---- dense data tables ------------------------------------------------ */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: .84rem; }
thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--surface-2); color: var(--heading);
  font-weight: 600; font-size: .78rem;
  text-align: left; padding: .38rem .55rem; border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
thead th a { color: inherit; text-decoration: none; display: inline-flex; gap: .25rem; }
thead th a:hover { color: var(--accent); }
tbody td { padding: .3rem .55rem; border-bottom: 1px solid var(--border-2); vertical-align: top; }
tbody tr:nth-child(even) td { background: var(--surface-2); }   /* zebra */
tbody tr:hover td { background: var(--accent-bg); }             /* teal-tint hover */
tbody tr:last-child td { border-bottom: 0; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
td.nowrap { white-space: nowrap; }
.mono { font-family: var(--font-mono); font-size: .95em; }

/* ---- toolbar / search ------------------------------------------------- */
.toolbar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: flex-end; margin-bottom: .7rem; }
.toolbar .grow { flex: 1 1 15rem; }

.listbar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem .9rem; margin-bottom: .7rem; }
.listbar .toolbar { flex: 1 1 auto; margin-bottom: 0; }
.listbar .pager-top { order: -1; flex: 0 0 17rem; width: 17rem; margin: 0 .3rem .15rem 0; }
.pager .step { min-width: 1.6rem; text-align: center; font-weight: 600; }

label { display: block; font-size: .74rem; color: var(--heading); font-weight: 600; margin-bottom: .15rem; }
input, select, textarea {
  width: 100%; padding: .3rem .5rem; height: 2.05rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text); font: inherit; font-size: .86rem;
}
select { height: 2.05rem; }
textarea { height: auto; }
/* Checkboxes and radios are not text fields. The sizing above would give them
   full width, a 2.05rem height and padding, turning each into a large empty box
   whose glyph sits away from its label. The attribute selector outranks the bare
   `input` selector, so this also holds inside the max-width:700px block below
   that re-heights inputs to 2.4rem. */
input[type=checkbox], input[type=radio] {
  width: auto; height: auto; padding: 0; margin: 0;
  flex: none; accent-color: var(--accent);
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent); outline-offset: 2px; border-color: var(--accent);
}

/* ---- buttons: navy structures, teal acts, coral alarms ---------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  height: 2.05rem; padding: 0 .95rem;
  background: var(--accent); color: #fff; border: 1px solid var(--accent);
  border-radius: var(--radius); font: inherit; font-size: .86rem; font-weight: 600;
  cursor: pointer; text-decoration: none; white-space: nowrap;
}
.btn:hover  { background: var(--accent-dk); border-color: var(--accent-dk); color: #fff; }
.btn:active { background: var(--uamh-teal-active); border-color: var(--uamh-teal-active); }
.btn.secondary { background: transparent; color: var(--heading); border-color: var(--heading); font-weight: 500; }
.btn.secondary:hover { background: var(--heading); color: var(--bg); }
.btn.danger { background: var(--uamh-coral); border-color: var(--uamh-coral); color: #fff; font-weight: 600; }
.btn.danger:hover { background: var(--uamh-coral-hover); border-color: var(--uamh-coral-hover); color: #fff; }

/* ---- key/value: two columns, tight ----------------------------------- */
/* align-items: baseline, not the default stretch. The label is 12.5px and the
   value 15px, so left to themselves the two boxes start at the same top edge
   and their BASELINES end up a few pixels apart — the value riding above the
   word naming it. Sharing a baseline is what makes a pair read as one line,
   and it holds whatever either font size later becomes. (Every dd in this app
   is inline content, so nothing here loses a stretched box it wanted.) */
.kv { display: grid; grid-template-columns: 10.5rem minmax(0,1fr); gap: .05rem .8rem; margin: 0;
      align-items: baseline; }
.kv dt { color: var(--text-dim); font-size: .78rem; }
.kv dd { margin: 0 0 .3rem; word-break: break-word; }
.kv.wide { grid-template-columns: 12rem minmax(0,1fr); }

/* ---- badges: status colour text on its tint, radius-sm --------------- */
.tag {
  display: inline-block; padding: .1rem .45rem; border-radius: var(--radius-sm);
  font-size: .7rem; font-weight: 600; letter-spacing: .01em; white-space: nowrap;
  background: var(--surface-2); color: var(--text-dim);
}
.tag.public { background: var(--accent-bg); color: var(--accent-dk); }
.tag.type   { background: var(--warn-bg);   color: var(--warn); }
.tag.rg1 { background: var(--success-bg); color: var(--success); }
.tag.rg2 { background: var(--warn-bg);    color: var(--warn); }
.tag.rg3 { background: var(--danger-bg);  color: var(--danger); }
.tag.agree { background: var(--success-bg); color: var(--success); }  /* NCBI agrees */
.tag.lag   { background: var(--warn-bg);    color: var(--warn); }     /* NCBI differs */

/* ---- taxonomy cross-check: NCBI verdict + external deep links --------- */
.xcheck { font-size: .82rem; line-height: 1.5; }
.xrefs { margin-top: .25rem; font-size: .78rem; }
.xrefs a { color: var(--accent); white-space: nowrap; }

/* ---- notices / callout ------------------------------------------------ */
.notice { padding: .5rem .75rem; border-radius: var(--radius); margin-bottom: .7rem; font-size: .84rem; }
.notice.error { background: var(--danger-bg);  color: var(--danger); }
.notice.warn  { background: var(--warn-bg);    color: var(--warn); }
.notice.ok    { background: var(--success-bg); color: var(--success); }
.notice.info  { background: var(--info-bg);    color: var(--heading); }

.callout { background: var(--uamh-callout); color: var(--text); border-radius: var(--radius); padding: 1rem 1.15rem; }
.callout h3 { margin-top: 0; color: var(--heading); }

.muted { color: var(--text-dim); font-size: .8rem; }
.empty { color: var(--text-dim); font-size: .82rem; padding: .5rem .55rem; font-style: italic; }
code { background: var(--accent-bg); color: var(--accent-dk); padding: .05rem .25rem; border-radius: var(--radius-sm); font-size: .9em; }

/* ---- pagination ------------------------------------------------------ */
.pager { display: flex; align-items: center; gap: .4rem; margin-top: .6rem; font-size: .82rem; }
.pager .spacer { margin-left: auto; color: var(--text-dim); }
.pager a, .pager span.cur {
  padding: .2rem .55rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  text-decoration: none; color: var(--text);
}
.pager span.cur { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
.pager a:hover { background: var(--surface-2); border-color: var(--accent); color: var(--accent-dk); }

/* ---- add-storage modal + box-map picker ------------------------------ */
dialog.modal {
  border: none; border-radius: var(--radius-lg); padding: 1rem 1.15rem 1.15rem;
  width: min(46rem, calc(100vw - 2rem));
  background: var(--surface); color: var(--text);
  box-shadow: 0 12px 44px rgba(28,56,102,.28);
}
dialog.modal::backdrop { background: rgba(28,56,102,.45); }
dialog.modal h3 { margin: 0; }
.modal [hidden] { display: none !important; }
.modal-row { display: flex; flex-wrap: wrap; gap: .5rem .8rem; margin: .1rem 0 .4rem; }
.modal-row .field { flex: 1 1 8rem; margin: 0; }
.pickmap {
  overflow: auto; max-height: 44vh; padding: .4rem;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-2);
}
.boxnav { display: flex; flex-wrap: wrap; gap: .25rem; margin: 0 0 .5rem; }
.boxchipnav {
  min-width: 2rem; padding: .12rem .4rem; font: inherit; font-size: .78rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text); cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.boxchipnav sup { font-size: .58rem; color: var(--text-dim); margin-left: .1rem; }
.boxchipnav:hover { border-color: var(--accent); }
.boxchipnav.sel { background: var(--accent); color: #fff; border-color: var(--accent); }
.boxchipnav.sel sup { color: rgba(255,255,255,.85); }
.boxchipnav:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.pgrid { display: grid; gap: 2px; grid-template-columns: repeat(var(--cols, 10), 2rem); width: max-content; }
/* Physical slot palette — a capacity read-out, kept in both themes. */
.pickslot {
  aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center;
  font: inherit; font-size: .6rem; padding: 0;
  border: 1px solid #c9d2da; border-radius: 3px; background: #fff; color: #5a6673;
  font-variant-numeric: tabular-nums;
}
.pickslot.s-empty { background: #cee5ff; color: #1C3866; cursor: pointer; }
.pickslot.s-empty:hover { outline: 2px solid var(--accent); outline-offset: -1px; }
.pickslot.s-low { background: #ffdddd } .pickslot.s-zero { background: #ff9999 }
.pickslot.s-missing { background: repeating-linear-gradient(45deg,#f0f0f0,#f0f0f0 4px,#e6e6e6 4px,#e6e6e6 8px); border-style: dashed }
.pickslot.sel { outline: 3px solid var(--accent); outline-offset: -1px; font-weight: 700; color: #1C3866; }

/* ---- origin locator map ----------------------------------------------
   Its own panel beside the Origin panel — equal height, same gap as the
   other panels. Self-contained: a vendored equirectangular world map
   (public-domain Natural Earth 110m) styled like the map on uamh.ca —
   light-grey land on a white ocean with a navy pin. No external requests. */
.origin-row { display: flex; align-items: stretch; gap: .8rem; margin-bottom: .8rem; }
.origin-row > .card { margin-bottom: 0; }
.origin-row .origin-main { flex: 1 1 0; min-width: 0; }
/* Desktop: the map panel is the same width as the Origin panel (50/50). The map
   fills the panel's full height as a fixed 2:1 (never stretches) and is centred
   horizontally, so spare width is padding that absorbs different Origin heights.
   A very tall Origin just crops the map's far edges rather than shrinking it. */
.card.geopanel { flex: 1 1 0; min-width: 0; position: relative; overflow: hidden; padding: 0; }
.card.geopanel .geomap {
  position: absolute; top: 0; bottom: 0; left: 50%; transform: translateX(-50%);
  width: auto; aspect-ratio: 2 / 1;
  background: #fff url(/assets/world.svg) center / 100% 100% no-repeat;
}
/* z-index only so the pin stays above the mask layer dark mode adds below. */
.card.geopanel .pin {
  position: absolute; width: 8px; height: 8px; margin: -4px 0 0 -4px;
  background: var(--uamh-navy); border-radius: 50%; z-index: 1;
}
/* Dark: the ocean is the card, the pin is coral, and the land is repainted.
   world.svg bakes one grey fill and CSS cannot reach inside a background image,
   so the land is painted as a colour and the map used as a MASK to cut it out —
   the same technique the dashboard map uses. One source file, either colour,
   and the file is untouched, so the dashboard and the billboard keep theirs.
   A browser that cannot mask keeps the background image and the land stays
   grey rather than disappearing.
   LIGHT MODE IS UNCHANGED: grey land, white ocean, navy pin. */
@media (prefers-color-scheme: dark) {
  :where(:root:not([data-theme="light"])) .card.geopanel .geomap { background-color: var(--surface); }   /* ocean = card bg #161F2E */
  :where(:root:not([data-theme="light"])) .card.geopanel .pin { background: var(--uamh-coral); }         /* coral: high visibility on the dark ocean */
  @supports (mask-image: url(/assets/world.svg)) or (-webkit-mask-image: url(/assets/world.svg)) {
    :where(:root:not([data-theme="light"])) .card.geopanel .geomap { background-image: none; }
    :where(:root:not([data-theme="light"])) .card.geopanel .geomap::before {
      content: ''; position: absolute; inset: 0; background: #223D52;
      -webkit-mask: url(/assets/world.svg) center / 100% 100% no-repeat;
              mask: url(/assets/world.svg) center / 100% 100% no-repeat;
    }
  }
}
@media screen {
  :where(:root[data-theme="dark"]) .card.geopanel .geomap { background-color: var(--surface); }   /* ocean = card bg #161F2E */
  :where(:root[data-theme="dark"]) .card.geopanel .pin { background: var(--uamh-coral); }
  @supports (mask-image: url(/assets/world.svg)) or (-webkit-mask-image: url(/assets/world.svg)) {
    :where(:root[data-theme="dark"]) .card.geopanel .geomap { background-image: none; }
    :where(:root[data-theme="dark"]) .card.geopanel .geomap::before {
      content: ''; position: absolute; inset: 0; background: #223D52;
      -webkit-mask: url(/assets/world.svg) center / 100% 100% no-repeat;
              mask: url(/assets/world.svg) center / 100% 100% no-repeat;
    }
  }
}

@media (max-width: 700px) {
  /* Stacked: size to content (flex:1 on a column would collapse the panels)
     and let the map be a full-width 2:1 block. */
  .origin-row { flex-direction: column; }
  .origin-row .origin-main { flex: 0 0 auto; }
  .card.geopanel { flex: 0 0 auto; position: static; }
  /* relative (not static) so the pin stays positioned against the map */
  .card.geopanel .geomap { position: relative; transform: none; inset: auto; width: 100%; height: auto; }
}

/* UNIFORM LINE SPACING in the two summary cards. Two separate things broke it.
   One: the dl is a grid item in a two-column grid, so a column with five rows
   beside one with six had the spare height handed to its OWN rows — 34.1px of
   pitch against 25.2px next to it. align-content leaves the slack at the foot
   instead. Two: a value long enough to wrap took its row to double height, so
   a narrower label column, which gives the longest value (the MySQL build
   string) the room to stay on one line. Not applied to .built below: its
   labels are sentences and it has the whole card width to itself. */
.panel-sheet .kv { align-content: start; }
.panel-sheet .grid.cols-2 .kv { grid-template-columns: 8.5rem minmax(0,1fr); }

/* THESE TWO LISTS DECIDE FOR THEMSELVES, like the rebuild dates below them.
   The global rule collapses every .grid.cols-2 to one column at 1100px — which
   is exactly the width where the cards STOP being half a window and become the
   whole of one. So the lists flattened into a single column at the moment they
   were handed twice the room, and a 1000px window showed one narrow column of
   counts with the rest of the card empty.

   auto-fit asks whether two will fit instead of asking how wide the window is.
   What "fit" means is per-card, because it is a fact about the CONTENT: the
   Collection card holds counts and the widest pair in it wants 201px, while the
   Environment card holds version strings and its widest wants 308px. One shared
   minimum would have to be the larger, which would hold the counts to a single
   column through every width where two would have fitted comfortably — the very
   complaint this rule exists to answer. So the default suits counts and the
   card with the long strings asks for more (see .longvals below). Both carry
   headroom for a value that grows: a couple of digits on a count, a longer
   build string on the version.

   min(…, 100%) is not decoration. A bare rem minimum is a floor the track keeps
   even when the CARD is narrower than the floor, and a phone would then scroll
   sideways by the difference. */
.panel-sheet .card > .grid.cols-2 {
  --list-min: 14rem;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--list-min), 100%), 1fr));
}
.panel-sheet .card > .grid.cols-2.longvals { --list-min: 21rem; }

/* The two derived copies at the foot of the Collection card, set off from the
   counts above them: they are not things the collection contains, they are
   jobs with a date on them.

   A WRAPPING ROW OF TWO PAIRS, not one grid of four columns. Both readings put
   the pairs side by side on a wide card and one per line on a narrow one; the
   difference is who decides. The four-column version had to be TOLD, by a
   media query, and the thing it needed to know was the width of this CARD —
   which is not the width of the window: it is the whole window below 1100px,
   where .grid.cols-2 collapses, and half of it above. Every attempt to name
   that in viewport terms left a band unguarded, and an unguarded band is not
   a near miss — the middle column goes NEGATIVE and the date sets itself one
   character per line into the label beside it. Twice.

   Flex wrapping asks the card directly and needs no numbers of its own. What
   a pair asks for is written out rather than rounded to a tidy figure, because
   THE BASIS IS THE WHOLE LAYOUT: at the exact width where two of them first fit
   side by side, each pair is precisely one basis wide, so the value track is
   precisely basis minus the label column. Anything wider than that wraps — in a
   band that sits immediately above the side-by-side threshold, wherever that
   threshold happens to fall. A basis that is short by six pixels does not fail
   by six pixels; it fails completely, in a band a few widths wide that no
   round-numbered spot check will land on. (17rem was short by six pixels. The
   band was 607-619px, thirteen widths, and a 22-width sweep stepped over it.)

   So: 9.3rem is the 8.5rem label column plus its .8rem gap, in rem because
   that is what those two are. 9.2em is the timestamp, in EM because that is
   what the timestamp is — em resolves against this element's own inherited
   font-size, so it follows body from 13.5px to the 15px the narrow breakpoint
   sets, which a rem-based basis cannot do. A tabular timestamp measures 8.95em
   at the larger of the two sizes; 9.2em carries it with a few pixels to spare
   in both.

   The side-by-side geometry comes out identical to the calc it replaces. Two
   pairs that grow equally each take (C - g)/2, so the second starts at
   C/2 + g/2 — the same place the right-hand column above it starts, because
   that is a 1fr 1fr grid with the same gap. Alignment by construction rather
   than by arithmetic that has to be redone whenever the label column moves. */
.panel-sheet .kv.built {
  margin-top: .55rem; padding-top: .45rem; border-top: 1px solid var(--border-2);
  display: flex; flex-wrap: wrap; column-gap: .8rem; row-gap: .25rem;
}
.panel-sheet .kv.built > div {
  flex: 1 1 calc(9.3rem + 9.2em);
  display: grid; grid-template-columns: 8.5rem minmax(0, 1fr); column-gap: .8rem;
  align-items: baseline;   /* not inherited from .kv — this is its own grid */
}
/* Tabular figures, as td.num and .chart-legend dd already use. Not decoration:
   the system font sets digits proportionally, so the SAME 16-character stamp
   measures anywhere from 102px to 122px depending on which digits it contains —
   a 20px spread, against a value track whose floor is fixed. Without this the
   answer to "does the date wrap here" depends on what time the job happened to
   run, which is not a question a stylesheet should be able to ask. With it,
   every stamp measures the same and the basis above can be sized once. It also
   puts the two dates' digits in the same columns, which is what you want from
   two timestamps sitting one above the other. */
.panel-sheet .kv.built dd { margin-bottom: 0; font-variant-numeric: tabular-nums; }
/* A breakdown of the line above it, not a total in its own right. */
.panel-sheet .kv dt.sub { font-weight: 400; opacity: .85; }

/* ---- dashboard world map ----------------------------------------------
   Collection above Environment in column one, the map beside them and as tall
   as both. `grid-auto-flow: column` fills down the first column before it
   starts the second, so the two panels keep their DOM order and only the map
   needs placing. */
.panel-sheet.has-map { grid-template-rows: auto auto; grid-auto-flow: column; }
.panel-sheet.has-map .mappanel { grid-row: 1 / span 2; }
@media (max-width: 1100px) {
  /* One column: the row span would stretch the map over a panel that is now
     below it rather than beside it. */
  .panel-sheet.has-map { grid-auto-flow: row; grid-template-rows: none; }
  .panel-sheet.has-map .mappanel { grid-row: auto; }
}

/* The same vendored equirectangular land as the strain locator (see
   .card.geopanel): grey coastlines baked into world.svg on a TRANSPARENT
   background, so the card's own colour is the ocean and dark mode needs no
   second file. The marks are an overlaid SVG in the same 360x180 space,
   stretched 100%/100% exactly as the background image is — one sizing rule for
   both, so a dot cannot drift off its coastline. */
.mappanel { display: flex; flex-direction: column; }
.mappanel .geomap-full {
  position: relative; width: 100%; aspect-ratio: 2 / 1; margin: auto 0;
  background: #fff url(/assets/world.svg) center / 100% 100% no-repeat;
}
.mappanel .geo-dots { position: absolute; inset: 0; width: 100%; height: 100%; }
.mappanel .geo-dots path { fill: none; stroke: var(--uamh-navy); }
@media (prefers-color-scheme: dark) {
  :where(:root:not([data-theme="light"])) .mappanel .geomap-full { background-color: var(--surface); }   /* ocean = card bg */
  /* Pantone 7406. The land is #223D52 and the ocean is the card, and a mark has
     to separate from BOTH: at L* 81 against the land's 25 this one does it on
     lightness as well as hue (6.9:1), so it survives colour-vision deficiency
     rather than leaning on hue alone. The land matches the strain page's
     locator — the two draw the same coastline, so they are one map. */
  :where(:root:not([data-theme="light"])) .mappanel .geo-dots path { stroke: var(--fig-dot); }
  /* world.svg is a background image with one baked fill, and CSS cannot reach
     inside a background image — so the land is painted as a colour and the map
     is used as a MASK to cut it out. One source file, either colour. If a
     browser cannot mask, @supports leaves it on the background image and the
     land stays grey rather than disappearing. */
  @supports (mask-image: url(/assets/world.svg)) or (-webkit-mask-image: url(/assets/world.svg)) {
    :where(:root:not([data-theme="light"])) .mappanel .geomap-full { background-image: none; }
    :where(:root:not([data-theme="light"])) .mappanel .geomap-full::before {
      content: ''; position: absolute; inset: 0; background: var(--fig-land);
      -webkit-mask: url(/assets/world.svg) center / 100% 100% no-repeat;
              mask: url(/assets/world.svg) center / 100% 100% no-repeat;
    }
  }
}
@media screen {
  :where(:root[data-theme="dark"]) .mappanel .geomap-full { background-color: var(--surface); }   /* ocean = card bg */
  /* Pantone 7406. The land is #223D52 and the ocean is the card, and a mark has
     to separate from BOTH: at L* 81 against the land's 25 this one does it on
     lightness as well as hue (6.9:1), so it survives colour-vision deficiency
     rather than leaning on hue alone. The land matches the strain page's
     locator — the two draw the same coastline, so they are one map. */
  :where(:root[data-theme="dark"]) .mappanel .geo-dots path { stroke: var(--fig-dot); }
  /* world.svg is a background image with one baked fill, and CSS cannot reach
     inside a background image — so the land is painted as a colour and the map
     is used as a MASK to cut it out. One source file, either colour. If a
     browser cannot mask, @supports leaves it on the background image and the
     land stays grey rather than disappearing. */
  @supports (mask-image: url(/assets/world.svg)) or (-webkit-mask-image: url(/assets/world.svg)) {
    :where(:root[data-theme="dark"]) .mappanel .geomap-full { background-image: none; }
    :where(:root[data-theme="dark"]) .mappanel .geomap-full::before {
      content: ''; position: absolute; inset: 0; background: var(--fig-land);
      -webkit-mask: url(/assets/world.svg) center / 100% 100% no-repeat;
              mask: url(/assets/world.svg) center / 100% 100% no-repeat;
    }
  }
}



/* ---- sequence display ------------------------------------------------ */
.seqblock {
  font-family: var(--font-mono);
  font-size: .74rem; line-height: 1.35; word-break: break-all;
  background: var(--surface-2); border: 1px solid var(--border-2);
  border-radius: var(--radius); padding: .4rem .5rem; max-height: 8rem; overflow: auto;
}

.login-wrap { max-width: 21rem; margin: 4rem auto; }
.login-brand { display: flex; align-items: center; gap: .6rem; justify-content: center; margin-bottom: 1.1rem; }
.login-brand .mark { height: 46px; width: auto; }
.login-brand .wordmark { height: 34px; width: auto; }
/* navy wordmark on the light login page, white wordmark in dark mode */
.login-brand .wm-dark { display: none; }
@media (prefers-color-scheme: dark) {
  :where(:root:not([data-theme="light"])) .login-brand .wm-light { display: none; }
  :where(:root:not([data-theme="light"])) .login-brand .wm-dark { display: block; }
}
@media screen {
  :where(:root[data-theme="dark"]) .login-brand .wm-light { display: none; }
  :where(:root[data-theme="dark"]) .login-brand .wm-dark { display: block; }
}


.login-wrap h1 { text-align: center; font-size: 1.15rem; }
.login-wrap .btn { margin-top: .8rem; }

/* ---- narrow screens: fallback only ----------------------------------- */
@media (max-width: 700px) {
  body { font-size: 15px; }
  main { padding: 1.2rem .6rem 2rem; }
  input, select, .btn { height: 2.4rem; }
  .kv, .kv.wide { grid-template-columns: 1fr; }
  /* Space before each label, because collapsing to one column puts the label
     UNDER the previous value and the pairs need telling apart. */
  .kv dt { margin-top: .35rem; }
  /* — but not in the two lists that do NOT collapse. Both rules below outrank
     the collapse (0,3,0 against 0,1,0), so those labels are still beside their
     values, and .35rem of margin there does not separate a pair: it shoves the
     label 5.6px down its own row, leaving every value floating above the word
     naming it. That was the alignment that looked off on a phone. */
  .panel-sheet .grid.cols-2 .kv dt, .panel-sheet .kv.built dt { margin-top: 0; }
  table { font-size: .82rem; }

}

/* ---- storage: box chips and slot grid --------------------------------
   Slot colours are carried over from the legacy rack/box pages so the
   visual language staff already know is preserved:
     full #ffffff  low #ffdddd  zero #ff9999  empty #cee5ff            */
.boxchip {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.1rem; padding: .15rem .4rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  font-size: .8rem; text-decoration: none; font-variant-numeric: tabular-nums;
}
.boxchip:hover { background: var(--accent-bg); border-color: var(--accent); color: var(--accent-dk); }
/* Shaded by free slots (blue = room to spare). A physical capacity read-out,
   so it keeps one light palette in both themes, like the slot grid. */
.boxchip.cap-0 { background: #ffffff; color: #17202a; border-color: #d6dde3; }
.boxchip.cap-1 { background: #eaf3fc; color: #17202a; border-color: #c4dcf3; }
.boxchip.cap-2 { background: #cee5ff; color: #17202a; border-color: #a9cff1; }
.boxchip.cap-3 { background: #9ecbf3; color: #12303f; border-color: #7db6e6; }
.boxchip sup { font-size: .6rem; color: #5a6673; margin-left: .12rem; }
/* legend swatches reuse the same shades */
.sw.cap-0 { background: #ffffff } .sw.cap-1 { background: #eaf3fc }
.sw.cap-2 { background: #cee5ff } .sw.cap-3 { background: #9ecbf3 }

.storage-legend {
  display: flex; flex-wrap: wrap; gap: .9rem; align-items: center;
  margin: 0 0 .6rem; font-size: .78rem; color: var(--text-dim);
}
.storage-legend i.sw {
  display: inline-block; width: .8rem; height: .8rem; margin-right: .3rem;
  border: 1px solid rgba(0,0,0,.25); vertical-align: -1px; border-radius: 2px;
}
.sw.s-full { background: #ffffff } .sw.s-low { background: #ffdddd }
.sw.s-zero { background: #ff9999 } .sw.s-empty { background: #cee5ff }

/* Cells are SQUARE: the grid is printed and laid over the physical box, so
   its geometry has to match the box, not the screen. --cols is set inline by
   the template; --pitch is the printed cell size in mm. */
.slotgrid {
  display: grid; gap: 2px;
  grid-template-columns: repeat(var(--cols, 10), minmax(3.6rem, 1fr));
  max-width: calc(var(--cols, 10) * 5.6rem);
}
.slot {
  border: 1px solid #c9d2da; border-radius: 3px;
  padding: .15rem .2rem;
  aspect-ratio: 1 / 1;
  display: flex; flex-direction: column; justify-content: center;
  font-size: .7rem; line-height: 1.2; text-align: center; color: #17202a;
}
.slot.s-full  { background: #ffffff }
.slot.s-low   { background: #ffdddd }
.slot.s-zero  { background: #ff9999 }
.slot.s-empty { background: #cee5ff }
.slot.s-missing { background: repeating-linear-gradient(45deg,#f0f0f0,#f0f0f0 4px,#e6e6e6 4px,#e6e6e6 8px); border-style: dashed }
.slot .slotno { color: #5a6673; font-size: .64rem }
.slot .slotid { display: block; font-weight: 700; text-decoration: none; color: #1C3866; font-size: .82rem }
.slot .slotid:hover { text-decoration: underline }
.slot .slotv { color: #5a6673; font-size: .63rem }

/* ---- printing a box map ---------------------------------------------- */
.printbar { display: flex; gap: .5rem; align-items: flex-end; margin: 0 0 .7rem; }
.printbar .hint { font-size: .78rem; color: var(--text-dim); max-width: 34rem; }
.printonly { display: none; }

@media print {
  @page { margin: 8mm; }
  body { background: #fff; color: #000; font-size: 9pt; }
  .topbar, .nav, .toolbar, .printbar, .pager,
  .storage-legend, .card.tight, h2, .btn { display: none !important; }
  main { max-width: none; padding: 0; margin: 0; }
  .card { border: 0; padding: 0; margin: 0; box-shadow: none; background: #fff; overflow: visible !important; }
  .printonly { display: block; margin: 0 0 3mm; font-size: 11pt; font-weight: 700; }

  .slotgrid {
    grid-template-columns: repeat(var(--cols, 10), var(--pitch, 12.7mm));
    gap: 0; max-width: none; width: max-content;
  }
  .slot {
    width: var(--pitch, 12.7mm); height: var(--pitch, 12.7mm);
    aspect-ratio: auto; border: .2mm solid #000; border-radius: 0;
    padding: 0; font-size: 5pt; line-height: 1.05; overflow: hidden;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  .slot .slotno { font-size: 4.2pt; color: #555 }
  .slot .slotid { font-size: 6pt; font-weight: 700; color: #000; text-decoration: none }
  .slot .slotv  { display: none }
  .slot.s-full  { background: #fff }
  .slot.s-low   { background: #ffdddd }
  .slot.s-zero  { background: #ff9999 }
  .slot.s-empty { background: #cee5ff }
  .slot.s-missing { background: #fff }
}

/* ---- clickable section headings -------------------------------------- */
h2 a.section-link { color: inherit; text-decoration: none; }
h2 a.section-link .count { color: var(--text-dim); font-weight: 400; }
h2 a.section-link:hover { color: var(--accent); }
h2 a.section-link:hover::after { content: " \203A"; color: var(--accent); }
h2 a.section-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

h2[id] { scroll-margin-top: .8rem; }
h2[id]:target { color: var(--accent); }
h2[id]:target::before { content: "\25B8\00A0"; color: var(--accent); }

/* Print menu on the strain detail header — a native <details> disclosure that
   drops the three strain printouts (production, herbarium, datasheet). */
.printmenu { position: relative; display: inline-block; }
.printmenu > summary { list-style: none; cursor: pointer; }
.printmenu > summary::-webkit-details-marker { display: none; }
.printmenu[open] > summary { background: var(--accent); color: #fff; border-color: var(--accent); }
.printmenu .menu {
  position: absolute; right: 0; top: calc(100% + .3rem); z-index: 30;
  min-width: 13rem; padding: .3rem; display: flex; flex-direction: column; gap: .1rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .18);
}
.printmenu .menu a {
  display: block; padding: .42rem .6rem; border-radius: var(--radius-sm);
  font-size: .85rem; color: var(--text); text-decoration: none; white-space: nowrap;
}
.printmenu .menu a:hover,
.printmenu .menu a:focus-visible { background: var(--surface-2); color: var(--accent); outline: none; }

/* datepick — flexible (year / month / day) date picker; see datepick.js. */
.dp-wrap { position: relative; display: flex; align-items: stretch; gap: .3rem; }
.dp-wrap > input { flex: 1 1 auto; min-width: 0; }
.dp-btn { flex: 0 0 auto; border: 1px solid var(--border); background: var(--surface);
  border-radius: var(--radius-sm); cursor: pointer; padding: 0 .55rem; line-height: 1; font-size: 1rem; }
.dp-btn:hover { border-color: var(--accent); }
.dp-pop { position: absolute; z-index: 60; top: calc(100% + .25rem); left: 0; width: 16rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .18); padding: .5rem; }
.dp-tabs { display: flex; gap: .2rem; margin-bottom: .45rem; }
.dp-tab { flex: 1; padding: .28rem 0; border: 1px solid var(--border); background: var(--surface);
  color: var(--text); border-radius: var(--radius-sm); cursor: pointer; font-size: .78rem; }
.dp-tab.on { background: var(--accent); color: #fff; border-color: var(--accent); }
.dp-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: .35rem; }
.dp-nav button { border: none; background: none; cursor: pointer; font-size: 1.15rem; line-height: 1;
  padding: .1rem .5rem; color: var(--text); border-radius: var(--radius-sm); }
.dp-nav button:hover { background: var(--surface-2); }
.dp-nav .dp-title { font-weight: 600; font-size: .85rem; }
.dp-grid { display: grid; gap: .25rem; }
.dp-grid.g3 { grid-template-columns: repeat(3, 1fr); }
.dp-grid.g4 { grid-template-columns: repeat(4, 1fr); }
.dp-grid.g7 { grid-template-columns: repeat(7, 1fr); }
.dp-dow { display: grid; grid-template-columns: repeat(7, 1fr); gap: .25rem; margin-bottom: .2rem; }
.dp-dow span { text-align: center; font-size: .62rem; color: var(--text-dim); }
.dp-cell { padding: .32rem 0; text-align: center; border-radius: var(--radius-sm); cursor: pointer;
  font-size: .8rem; background: var(--surface-2); color: var(--text); }
.dp-cell:hover { background: var(--accent); color: #fff; }
.dp-cell.sel { outline: 2px solid var(--accent); outline-offset: -2px; font-weight: 700; }
.dp-cell.dp-empty { background: none; cursor: default; }
.dp-foot { display: flex; justify-content: space-between; margin-top: .45rem; }
.dp-foot button { font-size: .75rem; background: none; border: none; color: var(--accent); cursor: pointer; padding: .1rem .2rem; }
.dp-foot button:hover { text-decoration: underline; }

/* Liquid-nitrogen panel on the Storage screen (SolutionBee scale levels). */
.ln2 { margin: 0 0 1.2rem; }
.ln2-head { display: flex; align-items: baseline; gap: .6rem; margin-bottom: .5rem; }
.ln2-head h2 { margin: 0; }
.ln2-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .8rem; }
.ln2-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: .7rem .85rem; position: relative; }
.ln2-card.stale { border-color: var(--warn); }
.ln2-name { display: flex; align-items: center; gap: .4rem; font-weight: 600; color: var(--heading); }
.ln2-dot { width: .7rem; height: .7rem; border-radius: 50%; flex: 0 0 auto; }
.ln2-level { font-size: 2rem; font-weight: 700; line-height: 1.1; margin: .15rem 0 .1rem; color: var(--text); }
.ln2-unit { font-size: .9rem; font-weight: 500; color: var(--text-dim); margin-left: .3rem; }
.ln2-spark { display: block; width: 100%; height: 30px; margin: .2rem 0 .35rem; }
.ln2-spark polyline { fill: none; stroke: var(--accent); stroke-width: 2; vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round; }
.ln2-card.stale .ln2-spark polyline { stroke: var(--warn); }
.ln2-meta { display: flex; flex-direction: column; gap: .1rem; font-size: .74rem; color: var(--text-dim); }
.ln2-flag { margin-top: .4rem; font-size: .72rem; color: var(--warn); font-weight: 600; }
@media (max-width: 720px) { .ln2-grid { grid-template-columns: 1fr; } }
.ln2-cap { margin: .1rem 0 .5rem; font-size: .8rem; }

/* Whole-row-clickable grids (see the tr[data-href] handler in layout.php). */
tr[data-href] { cursor: pointer; }
tr[data-href]:hover td { background: var(--accent-bg); }

/* Box-map slot hover: highlight the cell border and show a strain tooltip. */
.slot:not(.s-missing) { cursor: default; }
.slot:not(.s-missing):hover,
.slot.slot-active { border-color: var(--uamh-teal); box-shadow: inset 0 0 0 1px var(--uamh-teal); }
.slot-tip {
  position: fixed; z-index: 200; pointer-events: none; max-width: 18rem;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 6px 22px rgba(0, 0, 0, .22); padding: .4rem .55rem;
  font-size: .78rem; line-height: 1.35;
}
.slot-tip .tt-h  { font-weight: 700; color: var(--heading); }
.slot-tip .tt-sp { font-style: italic; }
.slot-tip .tt-m  { color: var(--text-dim); font-size: .72rem; margin-top: .1rem; }
.slot-tip .tt-c  { margin-top: .15rem; }

/* Writer-clickable slots: empty → assign a strain, filled → edit vials/comment. */
.slot.assignable, .slot.editable { cursor: pointer; }
.slot.assignable:hover { border-color: var(--uamh-teal); box-shadow: inset 0 0 0 1px var(--uamh-teal); }
/* Contents-table rows open the same slot-edit dialog. */
tr.editrow { cursor: pointer; }

/* Storage comments read as a dated log: keep their line breaks on display. */
td.cmt { white-space: pre-line; }
.slot-tip .tt-c { white-space: pre-line; }

/* ---- strain image gallery ------------------------------------------- */
/* Thumbnails are the 150px-tall cache the public catalogue also uses, so the
   tiles are sized to that height and let width vary with the original. */
.img-grid {
  display: flex; flex-wrap: wrap; gap: .6rem; margin-top: .2rem;
}
.img-tile { margin: 0; width: auto; }
.img-tile img {
  height: 150px; width: auto; display: block; cursor: zoom-in;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface);
}
.img-tile img:hover { border-color: var(--accent); }
.img-noprev {
  height: 150px; width: 120px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: .2rem;
  border: 1px dashed var(--border); border-radius: var(--radius-sm);
  text-decoration: none; color: var(--text); font-size: .8rem;
}
.img-tile figcaption {
  font-size: .75rem; color: var(--gray-600, #666); margin-top: .2rem;
  max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
dialog.img-modal { max-width: min(94vw, 1100px); }
dialog.img-modal img { max-width: 100%; max-height: 76vh; display: block; margin: 0 auto; }
.img-modal-cap { margin: .5rem 0 .2rem; font-size: .85rem; }

/* A compact button for section headers, where a full-size one would out-weigh
   the heading it sits beside. */
.btn.small { padding: .25rem .6rem; font-size: .8rem; }

/* The per-tile edit affordance stays quiet until the tile is hovered, so a
   read-heavy gallery is not littered with controls. */
.img-edit {
  border: 0; background: none; padding: 0 0 0 .35rem; cursor: pointer;
  font: inherit; font-size: .75rem; color: var(--accent); opacity: 0;
  transition: opacity .12s;
}
.img-tile:hover .img-edit, .img-edit:focus-visible { opacity: 1; }

/* Reordering is a deliberate mode: tiles only become draggable once asked. */
.img-grid.reordering .img-tile { cursor: grab; }
.img-grid.reordering .img-tile img { cursor: grab; pointer-events: none; }
.img-grid.reordering .img-tile.dragging { opacity: .4; }
.img-grid.reordering .img-tile { outline: 1px dashed var(--border); outline-offset: 3px; }

@media (max-width: 700px) {
  .img-tile img, .img-noprev { height: 110px; }
  .img-noprev { width: 90px; }
  .img-edit { opacity: 1; }        /* no hover on touch */
}

/* A locked taxon in the taxonomy grid. Muted so a screen of them does not
   read as a screen of warnings — it is a state, not a problem. */
.lockmark { font-size: .85em; opacity: .75; margin-left: .3rem; cursor: help; }

/* A cell that gives up its overflow rather than widening the table. The full
   value stays reachable through the title attribute. */
td.clip { max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- structured strain search -------------------------------------------- */
/* Wide enough to hold three columns of criteria without the fieldsets turning
   into a single tall stack, but capped so it never fills a large monitor. */
dialog.advsearch { max-width: min(96vw, 60rem); width: 60rem; }
.advsearch fieldset {
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: .6rem .8rem .8rem; margin: .8rem 0 0;
}
.advsearch legend { font-size: .8rem; font-weight: 600; color: var(--heading); padding: 0 .35rem; }
.advsearch label { display: block; font-size: .8rem; }
.advsearch label.inline {
  display: flex; align-items: flex-start; gap: .4rem; font-weight: 400; margin-top: .35rem;
}
.advsearch label.inline input { flex: 0 0 auto; margin-top: .15rem; }
.advsearch .grid { gap: .5rem .7rem; }
.advsearch .grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
/* The eight storage formats, as a compact block rather than eight rows. */
.advsearch .stgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .2rem .7rem; }
@media (max-width: 700px) {
  .advsearch .grid.cols-3, .advsearch .grid.cols-4 { grid-template-columns: 1fr 1fr; }
  .advsearch .stgrid { grid-template-columns: 1fr; }
}

/* One criterion: the field, how to match it, and what to match against. The
   field is widest because its names are the longest and the most read. */
.advsearch .srow {
  display: grid; grid-template-columns: 15rem 10rem minmax(0, 1fr) 2rem;
  gap: .4rem; margin-bottom: .4rem; align-items: center;
}
.advsearch .srow select, .advsearch .srow input { width: 100%; margin: 0; }
.advsearch .srow .rx { padding: 0; line-height: 1; }
.advsearch .srow .rv:disabled { background: var(--surface-2); color: var(--text-dim); }
#saddrow[disabled] { opacity: .5; cursor: not-allowed; }
@media (max-width: 700px) {
  /* Stacked, with the remove button back on the field's row. */
  .advsearch .srow { grid-template-columns: minmax(0, 1fr) 2rem; }
  .advsearch .srow .ro, .advsearch .srow .rv { grid-column: 1 / -1; }
}

/* ---- at a glance: live charts, University of Toronto palette -----------
   U of T Blue is the plinth rather than a data fill. On this dark interface
   #1E3765 as a mark would be near-invisible, so it carries the panels — the
   primary is present and structural — while the approved secondaries, which
   are bright by design, carry the data. Secondaries stay accents, and the
   numbers stay legible. */
.glance-title {
  margin: 1.4rem 0 .7rem; font-size: .95rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-dim);
}
/* THE SHEET'S OWN INK.
   The at-a-glance panels are modelled on the printed biobank sheet, and on
   paper they are always a near-white sheet with dark marks on it. On screen in
   dark mode they are not paper — they are cards, and a near-white slab in a
   dark page is a light source, not a document.
   So the sheet names its CHROME once, here — titles, legends, notes, key/value
   rows — and every rule below reads a token instead of a literal. Only these
   declarations know which way round it is.
   The FIGURES do not take part: they keep a near-white ground in either scheme
   (see the figure plate below and the SVG ink at the end of this file), because
   chart palette is built against a light ground and is shared verbatim with the
   printed sheet and the public site. */
:root {
  --sheet-bg: #fcfcfc; --sheet-head: #1E3765; --sheet-text: #1A2331;
  --sheet-dim: #5A6472; --sheet-label: #46505F; --sheet-grid: #D8D9D4;
  --sheet-track: #E8E9E4; --sheet-code-bg: #EEF1F4;
  --fig-ink: #1A2331; --fig-dim: #5A6472; --fig-grid: #D8D9D4; --fig-seg: #fff;
  /* The hairline around the Venn's circles. (The donut's wedge separators read
     --fig-seg, the same token as the sunburst's.) White on paper and on a light
     screen, where it reads as a boundary rather than as a fourth colour.
     --fig-edge-own is `initial` so that a circle given its OWN edge keeps it:
     an unset custom property makes var() take the fallback, which is the
     colour the chart chose. */
  --fig-edge: #fff; --fig-edge-own: initial;
  --fig-hub: #1E3765; --fig-name: #1E3765; --fig-count: #12303C;
  --fig-alpha: .5; --fig-proj: .16; --fig-plants: #EAC4D9; --fig-land: #C7C7C9; --fig-dot: var(--uamh-navy);
}
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    --sheet-bg: var(--surface); --sheet-head: var(--heading); --sheet-text: var(--text);
    --sheet-dim: var(--text-dim); --sheet-label: var(--text-dim); --sheet-grid: var(--border);
    --sheet-track: var(--surface-2); --sheet-code-bg: var(--surface-2);
    --fig-ink: var(--text); --fig-dim: var(--text-dim); --fig-grid: var(--border);
    --fig-seg: var(--surface); --fig-hub: var(--surface); --fig-name: var(--heading);
    --fig-count: #fff; --fig-alpha: .32; --fig-proj: .34;
    --fig-plants: #AB1368; --fig-land: #223D52; --fig-dot: #F1C500;
    /* NO OUTLINE ON THE VENN'S CIRCLES. The white edge was drawn for a light
       ground: on a dark one it is the brightest ink in the figure, so the eye
       reads the scaffolding before the quantity. The lenses are already
       separated by hue and, since --fig-alpha dropped to .32, by value as well
       — the boundary was doing work the colour now does. Both the default edge
       and any a chart chose for itself go. The donut is NOT blanked this way:
       its separators take --fig-seg, the card's own colour, so they read as a
       gap between wedges rather than as a line over them. */
    --fig-edge: transparent; --fig-edge-own: transparent;
  }
}
/* @media screen, because Chrome forces prefers-color-scheme: light when it
   prints. That makes the media-query copy above inert on paper for free; an
   attribute selector keeps matching, so without this a reader who chose Dark
   would print --heading #CDDDF2 on white paper and lose every heading. The
   two forms of dark must behave the same in EVERY context, not just on a
   screen. */
@media screen {
  :root:where([data-theme="dark"]) {
      --sheet-bg: var(--surface); --sheet-head: var(--heading); --sheet-text: var(--text);
      --sheet-dim: var(--text-dim); --sheet-label: var(--text-dim); --sheet-grid: var(--border);
      --sheet-track: var(--surface-2); --sheet-code-bg: var(--surface-2);
      --fig-ink: var(--text); --fig-dim: var(--text-dim); --fig-grid: var(--border);
      --fig-seg: var(--surface); --fig-hub: var(--surface); --fig-name: var(--heading);
      --fig-count: #fff; --fig-alpha: .32; --fig-proj: .34;
      --fig-plants: #AB1368; --fig-land: #223D52; --fig-dot: #F1C500;
      /* NO OUTLINE ON THE VENN'S CIRCLES. The white edge was drawn for a light
         ground: on a dark one it is the brightest ink in the figure, so the eye
         reads the scaffolding before the quantity. The lenses are already
         separated by hue and, since --fig-alpha dropped to .32, by value as well
         — the boundary was doing work the colour now does. Both the default edge
         and any a chart chose for itself go. The donut is NOT blanked this way:
         its separators take --fig-seg, the card's own colour, so they read as a
         gap between wedges rather than as a line over them. */
      --fig-edge: transparent; --fig-edge-own: transparent;
  }
}

/* PAPER IS ALWAYS PAPER. `.sheet` is the print preview — the same partial the
   dashboard renders, wrapped for letter portrait — and @media print is the page
   itself. Both force the light set back regardless of the reader's scheme,
   because a dark-mode operating system must not put dark panels on paper. */
.sheet, .sheet .glance, .sheet .panel-sheet {
  color-scheme: light;   /* paper, even inside a dark document */
  --sheet-bg: #fcfcfc; --sheet-head: #1E3765; --sheet-text: #1A2331;
  --sheet-dim: #5A6472; --sheet-label: #46505F; --sheet-grid: #D8D9D4;
  --sheet-track: #E8E9E4; --sheet-code-bg: #EEF1F4;
  --fig-ink: #1A2331; --fig-dim: #5A6472; --fig-grid: #D8D9D4; --fig-seg: #fff;
  /* The hairline around the Venn's circles. (The donut's wedge separators read
     --fig-seg, the same token as the sunburst's.) White on paper and on a light
     screen, where it reads as a boundary rather than as a fourth colour.
     --fig-edge-own is `initial` so that a circle given its OWN edge keeps it:
     an unset custom property makes var() take the fallback, which is the
     colour the chart chose. */
  --fig-edge: #fff; --fig-edge-own: initial;
  --fig-hub: #1E3765; --fig-name: #1E3765; --fig-count: #12303C;
  --fig-alpha: .5; --fig-proj: .16; --fig-plants: #EAC4D9; --fig-land: #C7C7C9; --fig-dot: var(--uamh-navy);
}
@media print {
  :root {
    --sheet-bg: #fcfcfc; --sheet-head: #1E3765; --sheet-text: #1A2331;
    --sheet-dim: #5A6472; --sheet-label: #46505F; --sheet-grid: #D8D9D4;
    --sheet-track: #E8E9E4; --sheet-code-bg: #EEF1F4;
    --fig-ink: #1A2331; --fig-dim: #5A6472; --fig-grid: #D8D9D4; --fig-seg: #fff;
  /* The hairline around the Venn's circles. (The donut's wedge separators read
     --fig-seg, the same token as the sunburst's.) White on paper and on a light
     screen, where it reads as a boundary rather than as a fourth colour.
     --fig-edge-own is `initial` so that a circle given its OWN edge keeps it:
     an unset custom property makes var() take the fallback, which is the
     colour the chart chose. */
  --fig-edge: #fff; --fig-edge-own: initial;
    --fig-hub: #1E3765; --fig-name: #1E3765; --fig-count: #12303C;
    --fig-alpha: .5; --fig-proj: .16; --fig-plants: #EAC4D9; --fig-land: #C7C7C9; --fig-dot: var(--uamh-navy);
  }
}
.glance .card, .panel-sheet > .card, .panel-sheet .ln2-card {
  /* Border and radius are inherited from .card unchanged. In dark mode the fill
     resolves to var(--surface), which is exactly what .card already uses — the
     panel simply stops being a special case. */
  background-color: var(--sheet-bg);
}
/* THE FIGURE KEEPS ITS PAPER. In dark mode each chart sits on a near-white
   plate inside the dark card, the way a printed figure is inset into a page.
   `.chart-bars` and `.chart-donut` are overflow:visible, so marks are drawn
   outside the viewBox and the padding is what keeps them on the plate rather
   than half on the dark card.
   The map is excluded: it draws its own ocean and already answers to the scheme
   itself (see .mappanel above). The LN sparklines are excluded too — they are a
   single accent stroke with no palette to protect. */
/* (panel ink moved to the end of this file — see below) */
.glance .card h3 {
  margin: 0 0 .7rem; font-size: .8rem; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text);
}
.glance-wide { grid-column: 1 / -1; }
.glance-body { display: flex; gap: 1rem; align-items: flex-start; flex-wrap: wrap; }
.glance-body > svg { flex: 0 0 auto; }
.glance-note {
  margin: .7rem 0 0; font-size: .74rem; line-height: 1.45; color: var(--text-dim);
}

/* Three columns: label, absolute number, share. Number and percentage in one
   cell right-align as a single ragged string — "7,475 57.6%" over "111 0.9%" —
   so neither column can be read down. Separated, both align on their own. */
.chart-legend {
  display: grid; grid-template-columns: minmax(0,1fr) auto auto; gap: .1rem .7rem;
  margin: 0; flex: 1 1 13rem; align-content: start;
}
.chart-legend dd.pct { color: var(--text-dim); min-width: 2.6rem; }
.chart-legend dt { font-size: .76rem; color: var(--text-dim); display: flex; align-items: center; }
.chart-legend dd { margin: 0; font-size: .76rem; text-align: right; font-variant-numeric: tabular-nums; }
.chart-dot {
  display: inline-block; width: .55rem; height: .55rem; border-radius: 50%;
  margin-right: .4rem; flex: 0 0 auto;
  /* The swatch has to match the shape it names, and one of these palettes is
     drawn on paper and the other on a dark card. The fallback keeps any dot
     that predates this working. */
  background: var(--dot, currentColor);
}
@media (prefers-color-scheme: dark) {
 :where(:root:not([data-theme="light"])) .chart-dot { background: var(--dot-dark, var(--dot, currentColor)); } 
}
 @media screen {
  :where(:root[data-theme="dark"]) .chart-dot { background: var(--dot-dark, var(--dot, currentColor)); }
}
 

.sheet .chart-dot { background: var(--dot, currentColor); }
@media print { .chart-dot { background: var(--dot, currentColor); } }

/* SVG-internal type. Fill, not color: SVG text does not inherit `color`. */
/* A wedge percentage is an annotation on the shape it sits in, not a heading
   for it: at 13px it outweighed the 12.16px legend it is read against, so it
   stays a step below. .chart-venn-n matches it — the Venn's numbers are the
   same kind of mark. (On the printed sheet both resolve to 5.4pt through
   --ink; this is the screen.) */
.chart-slice-label { fill: #fff; font-size: 10px; font-weight: 600; }
/* Pale slices take dark ink instead. A wheel drawn in tints of one colour runs
   from a fill white reads well on to one it does not read on at all — white on
   the palest tint of Pantone 2985 is 1.1:1 — so Chart::donut() decides per
   slice from the fill's own luminance and tags the ones that need this. */
.chart-slice-label.on-light { fill: #12303C; }
.chart-centre      { fill: var(--text); font-size: 15px; font-weight: 700; }
.chart-centre-sub  { fill: var(--text-dim); font-size: 10px; letter-spacing: .04em; }
/* Axis type matches the legends beside it — they are read together, and a
   smaller axis made the chart look like a lesser thing than its neighbours. */
.chart-axis        { fill: var(--text-dim); font-size: 14px; }
.chart-axis-title  { fill: var(--text-dim); font-size: 13px; font-weight: 600;
                     letter-spacing: .08em; text-transform: uppercase; }
.chart-grid        { stroke: var(--border); stroke-width: 1; stroke-dasharray: 2 3; }
.chart-bars, .chart-donut { overflow: visible; }

/* Overlapping categories: bars that deliberately do not sum to 100%. */
.euler { flex: 1 1 15rem; display: grid; gap: .45rem; }
.euler-row { display: grid; grid-template-columns: 8.5rem minmax(0,1fr) 2.4rem; align-items: center; gap: .5rem; }
.euler-name { font-size: .76rem; color: var(--text-dim); display: flex; align-items: center; }
.euler-track { height: .55rem; border-radius: 99px; background: var(--surface-2); overflow: hidden; }
.euler-fill { display: block; height: 100%; border-radius: 99px; }
.euler-val { font-size: .74rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--text-dim); }

@media (max-width: 1100px) {
  .euler-row { grid-template-columns: 7rem minmax(0,1fr) 2.2rem; }
}

/* sunburst: depth is tint of the parent's hue, so a phylum groups visually
   with its classes and orders without leaving the approved palette. */
.chart-sun-seg { stroke: var(--surface); stroke-width: .6; }
.chart-sun-hub     { fill: #fff; font-size: 11px; font-weight: 700; letter-spacing: .04em; }
.chart-sun-hub-sub { fill: #E4EAF4; font-size: 9px; font-weight: 700; }

/* Title pinned at the top, figure centred in whatever space is left, note at
   the bottom. Cards in a grid row stretch to the tallest, so without this a
   short chart sits hard under its heading with a pool of empty panel beneath
   it. `margin-block:auto` on the figure takes the slack evenly above and below;
   the heading and the note stay put because they do not. */
.glance .card { display: flex; flex-direction: column; }
.glance .card > h3           { flex: 0 0 auto; }
.glance .card > .glance-note { flex: 0 0 auto; margin-bottom: 0; }
.glance .card > .glance-body,
.glance .card > svg          { margin-block: auto; }
.glance .card > .glance-body { align-items: center; }

/* Panel ink, LAST so it wins.
   These sat immediately after the .glance .card background rule and were
   silently overridden by the base .glance .card h3, .panel-sheet .card h2, .panel-sheet .card h3 further down the file:
   equal specificity, later declaration. The titles went near-invisible —
   dark-mode light ink on a near-white sheet. Order is the whole fix. */
.glance .card h3, .panel-sheet .card h2, .panel-sheet .card h3          { color: var(--sheet-head); }
.glance .glance-note, .panel-sheet .glance-note      { color: var(--sheet-dim); }
.glance .chart-legend dt, .panel-sheet .chart-legend dt  { color: var(--sheet-label); }
.glance .chart-legend dd, .panel-sheet .chart-legend dd  { color: var(--sheet-text); }
.glance .chart-legend dd.pct, .panel-sheet .chart-legend dd.pct { color: var(--sheet-dim); }
.glance .euler-name, .panel-sheet .euler-name       { color: var(--sheet-label); }
.glance .euler-val, .panel-sheet .euler-val        { color: var(--sheet-dim); }
.glance .euler-track, .panel-sheet .euler-track      { background: var(--sheet-track); }
.glance .muted, .panel-sheet .muted            { color: var(--sheet-dim); }
/* SVG ink: these are fills, and none of them inherit `color`.
   Read the --fig-* tokens, so a figure is drawn for whichever ground it is on.
   The awkward part is the TRANSLUCENT fills: the Venn's circles and the
   sunburst's outer rings take their apparent colour from what is behind them,
   so on a dark card the same alpha that reads as a pale wash on paper lands in
   the mid-tones, where neither white nor dark ink reaches 4.5:1. The fix is not
   ink but alpha — see --fig-alpha below, which is what makes white legible on
   all seven Venn regions. */
.glance .chart-centre, .panel-sheet .chart-centre     { fill: var(--fig-ink); }
.glance .chart-centre-sub, .panel-sheet .chart-centre-sub { fill: var(--fig-dim); }
.glance .chart-axis, .panel-sheet .chart-axis       { fill: var(--fig-dim); }
.glance .chart-axis-title, .panel-sheet .chart-axis-title { fill: var(--fig-dim); }
.glance .chart-grid, .panel-sheet .chart-grid       { stroke: var(--fig-grid); }
.glance .chart-sun-seg, .panel-sheet .chart-sun-seg { stroke: var(--fig-seg); stroke-width: .7; }
/* The hub is U of T Blue on paper. On the dark card that is within a few points
   of the card itself, so it becomes the card — the centre reads as a hole
   punched through the rings, which is what a sunburst centre is. */
.glance .chart-sun-hub-disc, .panel-sheet .chart-sun-hub-disc { fill: var(--fig-hub); }
/* Alpha, not ink. At .5 over a dark card the two-set lenses composite to
   mid-tones (worst 1.03:1 against white); at .32 every region clears 6:1 and
   the hues stay themselves. Plants is a 25% tint on paper — a tint mixes toward
   WHITE, so it stays pale on any ground and cannot be told from the sky blue
   beside it; on the dark card it is the Pantone at full strength. */
/* The wedge boundaries, in --fig-seg: the SAME token the sunburst's segments
   already use, because it is the same job. White on paper; on the dark card it
   is the card's own colour, so the separator stops being a line drawn over the
   ring and becomes a gap cut through it — the ground showing between two
   wedges rather than a third ink laid on top of them.
   `path` and not `*`: the donut also draws a <circle> for the empty and the
   single-category cases, and there the stroke IS the ring — blanking it would
   blank the chart. */
.glance .chart-donut path { stroke: var(--fig-seg); }
/* :not(.has-edge) is the convention glance_print.php already uses when it
   recolours these boundaries for paper; a circle given its own edge is a
   decision, so it is driven by its own token rather than this one. */
.glance .chart-venn-set:not(.has-edge) { stroke: var(--fig-edge); }
.glance .chart-venn-set.has-edge       { stroke: var(--fig-edge-own, var(--venn-own-edge)); }
.glance .chart-venn-set   { fill-opacity: var(--fig-alpha); }
.glance .chart-venn-set.has-edge { fill: var(--fig-plants); }
.glance .chart-bar-projected     { fill-opacity: var(--fig-proj); }

/* The same ink for the panels outside the glance grid: Collection, Environment
   and the tank cards. Their own rules use var(--text)/--heading/--text-dim,
   which are LIGHT in dark mode and vanish on a near-white sheet. */
.panel-sheet .kv dt,  .panel-sheet .ln2-meta, .panel-sheet .ln2-unit { color: var(--sheet-dim); }
.panel-sheet .kv dd,  .panel-sheet .ln2-level                                   { color: var(--sheet-text); }
.panel-sheet .card h2, .panel-sheet .ln2-name                                   { color: var(--sheet-head); }
.panel-sheet code                                { background: var(--sheet-code-bg); color: var(--sheet-head); }
.panel-sheet .ln2-card                           { border-color: var(--border); }
/* The size and the weight of .chart-slice-label, so the two read as one kind
   of mark: at an identical size the heavier of the two still looks larger. */
.glance .chart-venn-n    { fill: var(--fig-count); font-size: 10px; font-weight: 600; }
/* Same size as the legend text beside it — they are read together. */
.glance .chart-venn-name { fill: var(--fig-name); font-size: 12px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; }

/* Collection and Environment wear the same title as the at-a-glance panels:
   the glance uses h3 and these use h2, which is right semantically, so this
   matches them typographically rather than changing the markup. */
.panel-sheet .card h2 {
  font-size: .8rem; letter-spacing: .06em; text-transform: uppercase;
  font-weight: 700; margin: 0 0 .7rem;
}

/* Two columns of legend, for a chart with more entries than one column can
   carry at a readable size. Each column keeps its own label / number / share
   alignment rather than being one list wrapped in half. */
.legend-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.1rem; flex: 1 1 21rem; }
@media (max-width: 1100px) { .legend-pair { grid-template-columns: 1fr; } }

/* Present to assistive technology, absent to the eye. Used where the visible
   design carries a label some other way — the dashboard's page title is in the
   navigation highlight, which a screen reader does not announce as a heading. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* The print link rides on the section heading rather than sitting in a bar of
   its own — it is one action, and a toolbar for one action is furniture. */
.glance-print {
  float: right; font-size: .72rem; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; text-decoration: none; color: var(--accent);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .1rem .5rem; margin-top: -.1rem;
}
.glance-print:hover { color: var(--accent-dk); border-color: var(--accent); }
/* On the printed sheet the heading is hidden, and with it this link. */

/* The grid's own gap spaces these cards; their individual bottom margin only
   extends the grid's box 13px past the last card, which made the heading below
   it sit further away than the same heading elsewhere. Measured: 35px against
   22px. The gap does the spacing, so the margin goes. */
.grid.panel-sheet > .card { margin-bottom: 0; }
/* A GRID ALREADY SPACES ITS CHILDREN. `.card` carries a bottom margin for the
   stacked case, where it is the only thing between one card and the next; in a
   grid it lands on top of the row gap, so rows sat twice as far apart as
   columns — 25.6px against 12.8px. Invisible on a one-row grid, which is why
   it survived: the glance is the only card grid here deep enough to show it.
   `.grid.panel-sheet` above and `.origin-row` do the same thing one at a time;
   the day a fourth appears, make it `.grid > .card` and give the grid itself
   the trailing margin the last row would lose. */
.glance > .card { margin-bottom: 0; }
.chart-legend dt.lead, .chart-legend dd.lead { margin-top: .3rem; }
