  * { box-sizing: border-box; }
  html, body {
    margin: 0; padding: 0;
    background: #ffffff;
    color: #222;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
    font-size:16px;
    line-height: 1.55;
    /* Prevent stray horizontal scroll. Any content wider than the
       viewport will get clipped instead of triggering a page-wide
       horizontal scrollbar (most often this comes from a wide table
       or a long unbreakable string). Tables use per-column ellipsis
       truncation (.doc-id, .patient-id, .collection-date) to stay
       within the container — no inline scrollbars anywhere. */
    overflow-x: hidden;
  }

  /* -------- Header -------- */
  header {
    padding: 16px 28px;
    border-bottom: 1px solid #ddd;
    display: flex;
    align-items: center;
    /* Allow the button cluster to wrap onto a second row on narrow
       viewports (iPad portrait / phones) instead of overflowing or
       squashing — see the @media block below for the shrink rules. */
    flex-wrap: wrap;
    gap: 16px;
    background: #fafafa;
    position: sticky; top: 0; z-index: 50;
  }
  header h1 { margin: 0; font-size:20px; font-weight: 600; }
  /* Brand logos. height fixed; width auto-scales preserving aspect.
     The two PNGs are served from /static/ — see app.py StaticFiles mount. */
  header .brand-logo {
    height: 36px; width: auto;
    display: block;
  }
  /* Title text next to the left logo. */
  header .brand-title {
    margin: 0;
    font-size:18px;
    font-weight: 600;
    color: #222;
    letter-spacing: 0.01em;
    white-space: nowrap;
  }
  /* Empty span that takes up all the slack between the title (left)
     and the buttons (right). flex:1 means it absorbs any extra space,
     pushing everything after it to the far right. Cleaner than putting
     `margin-left: auto` on each individual element. */
  header .header-spacer { flex: 1; }
  header .project-logo {
    height: 32px; width: auto;
    display: block;
  }
  header .path {
    color: #888;
    font-size:14px;
    margin-left: auto;
  }
  header .clock {
    color: #555;
    font-size:14px;
    padding: 4px 10px;
    border: 1px solid #ddd;
    border-radius: 3px;
    background: #fff;
  }
  header .btn {
    font-size:14px;
    text-decoration: none;
    /* Belt-and-suspenders with the &#xFE0E; in the markup: ask the UA
       to render any symbol glyph as text, not color emoji (Safari/iOS). */
    font-variant-emoji: text;
  }

  /* Mobile / iPad-portrait header: the brand block (logo + title) keeps
     the first row; the button cluster wraps to its own row below and
     shrinks so the buttons fit a couple-up without overflowing or
     squashing. The .header-spacer (which becomes full-width here) forces
     that wrap by filling the rest of row one. */
  @media (max-width: 820px) {
    header {
      padding: 12px 16px;
      gap: 8px;
    }
    header .brand-title { font-size:16px; }
    /* Spacer fills the rest of row one, pushing the buttons to row two. */
    header .header-spacer { flex-basis: 100%; height: 0; }
    header .btn {
      font-size:13px;
      padding: 5px 8px;
    }
    /* The wall-clock isn't essential on small screens and eats width. */
    header .clock { display: none; }
  }
  /* Very narrow phones: drop the title text, keep just the brand logo. */
  @media (max-width: 480px) {
    header .brand-title { display: none; }
  }

  .container { padding: 24px 28px 80px; max-width: 1500px; margin: 0 auto; }
  /* Tighten side padding on narrow viewports so content doesn't hug
     the edge but also doesn't waste 28px × 2 of horizontal space we
     don't have. */
  @media (max-width: 700px) {
    .container { padding: 16px 12px 60px; }
  }

  /* -------- Footer -------- */
  /* Sits at the end of .container so it inherits the same max-width
     and centering as the rest of the page content. The container's
     bottom padding (80px) gives breathing room before the copyright
     line; a top border separates it from the last section. */
  .page-footer {
    margin-top: 48px;
    padding-top: 18px;
    border-top: 1px solid #eee;
    text-align: center;
    font-size:13px;
    color: #999;
    letter-spacing: 0.02em;
  }

  /* -------- Section -------- */
  h2 {
    margin: 32px 0 14px;
    font-size:18px;
    font-weight: 600;
    color: #333;
    display: flex; align-items: center; gap: 10px;
  }
  h2 .sub { font-weight: 400; font-size:14px; color: #888; }
  h2 .actions { margin-left: auto; display: flex; gap: 8px; }

  /* -------- System status: 4 cards (Allocator + CPU/Memory/Disk).
     Was 8 columns when the 4 state-counts lived here; shrunk to 4 since
     those moved to their own jobs-counts-row above. */
  .status-row {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
  }
  @media (max-width: 600px) {
    .status-row { grid-template-columns: repeat(2, 1fr); }
  }
  .card {
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 14px 14px;
  }
  .card .label {
    font-size:13px; color: #777; margin-bottom: 6px;
    text-transform: uppercase; letter-spacing: 0.04em;
  }
  .card .value {
    font-size: 22px; font-weight: 600;
  }
  .card .sub { font-size:13px; color: #888; margin-top: 4px; }
  .card.ok .value { color: #2e7d32; }
  .card.err .value { color: #c62828; }
  .card.warn .value { color: #ef6c00; }
  /* Clickable variant for top-of-page count cards (Running / Queued / Cooldown
     / Permanent fails). Tells the user they can drill in. */
  .card.clickable { cursor: pointer; transition: background 0.15s, border-color 0.15s; }
  .card.clickable:hover { background: #fafbfc; border-color: #aaa; }
  .card.clickable .hint {
    font-size:12px; color: #999; margin-top: 6px;
    text-transform: none; letter-spacing: 0;
  }

  /* -------- Period toggle (week / month) -------- */
  .period-toggle {
    display: inline-flex; gap: 0; margin-left: 12px;
    border: 1px solid #ccc; border-radius: 4px; overflow: hidden;
    vertical-align: middle;
  }
  .period-toggle button {
    background: #fff; border: 0; padding: 4px 10px; font-size:13px;
    cursor: pointer; color: #666;
  }
  .period-toggle button.active {
    background: #1976d2; color: #fff;
  }
  .period-toggle button:not(.active):hover { background: #f0f0f0; }

  /* -------- Sysadmin commands card grid -------- */
  .sysadmin-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
    gap: 12px;
  }
  .sysadmin-card {
    background: #fff; border: 1px solid #ddd; border-radius: 4px;
    padding: 12px 14px;
  }
  .sysadmin-card h4 {
    margin: 0 0 4px 0; font-size:15px; color: #333;
  }
  .sysadmin-card p {
    margin: 0 0 8px 0; font-size:13px; color: #666; line-height: 1.5;
  }
  .sysadmin-card pre {
    background: #2b2b2b; color: #e0e0e0; padding: 8px 10px; border-radius: 3px;
    font-size:13px; margin: 0; overflow-x: auto; white-space: pre-wrap;
    word-break: break-all;
  }
  .sysadmin-card .copy-row {
    display: flex; justify-content: space-between; align-items: center;
    margin-top: 6px;
  }
  .sysadmin-card .copy-row span { font-size:12px; color: #888; }
  .sysadmin-card button.copy-btn {
    background: #f5f5f5; border: 1px solid #ccc; border-radius: 3px;
    padding: 3px 10px; font-size:13px; cursor: pointer;
  }
  .sysadmin-card button.copy-btn:hover { background: #eee; }
  .sysadmin-card button.copy-btn.copied { background: #c8e6c9; border-color: #66bb6a; }

  /* Notifications log table */
  .notif-table { width: 100%; border-collapse: collapse; font-size:14px; }
  .notif-table th, .notif-table td {
    padding: 8px 10px; border-bottom: 1px solid #eee; vertical-align: top;
  }
  .notif-table th {
    text-align: left; background: #fafafa; font-weight: 600;
    font-size:13px; color: #555; text-transform: uppercase;
    letter-spacing: 0.03em;
  }
  .notif-table td.status-ok { color: #2e7d32; font-weight: 600; }
  .notif-table td.status-failed { color: #c62828; font-weight: 600; }
  .notif-table td.status-skipped { color: #999; }
  .notif-table .detail {
    max-width: 420px; color: #777; font-size:13px; line-height: 1.4;
    word-break: break-word;
  }

  /* -------- Charts -------- */
  .grid-charts {
    display: grid;
    grid-template-columns: 1.3fr 1fr;
    gap: 16px;
  }
  @media (max-width: 1000px) {
    .grid-charts { grid-template-columns: 1fr; }
  }
  .chart-box {
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 16px;
  }
  .chart-box h3 {
    margin: 0 0 12px;
    font-size:15px;
    font-weight: 600;
    color: #555;
  }
  .chart-wrap { position: relative; height: 240px; }

  /* -------- Tables -------- */
  /* Section containers that hold tables/lists. Horizontal scroll is
     intentionally NOT enabled — the dashboard must fit within the
     viewport at all times. Any column that risks overflowing uses
     ellipsis truncation (.doc-id, .patient-id, .collection-date) with
     a `title` attribute for hover-to-reveal. If a new wide column is
     added, give it a max-width + ellipsis class rather than reaching
     for overflow-x. */
  #cooldowns-box, #commands-box, #notifications-box, #repos-box {
    max-width: 100%;
  }

  table {
    width: 100%;
    border-collapse: collapse;
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 4px;
    overflow: hidden;
    font-size:15px;
  }
  th, td {
    text-align: left;
    padding: 10px 14px;
    border-bottom: 1px solid #eee;
  }
  th {
    background: #f5f5f5; color: #555;
    font-weight: 600; font-size:13px;
    text-transform: uppercase; letter-spacing: 0.04em;
  }
  tr:last-child td { border-bottom: none; }
  tr.clickable { cursor: pointer; }
  tr.clickable:hover { background: #f8f9fa; }
  td.doc-id { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Same ellipsis-truncation pattern as .doc-id. Kept narrow because
     Process Queue has 10 columns crammed into the page-width container
     (no horizontal scroll permitted), so the patient_id column trades
     full display for room — operators see the first ~20 chars, hover
     the cell to read the full id via the `title` attribute.
     Replaces an older `.slice(0, 16)` JS hack that silently chopped
     patient_ids without showing the user. */
  td.patient-id {
    max-width: 160px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Same ellipsis-truncation pattern as .doc-id / .patient-id. The
     collection_date field is a free-form string from Firestore — often
     an ISO timestamp like "2024-04-16T16:00:00.000Z" — and rendering
     it raw blows out the column. We don't parse it (the format is the
     source of truth), we just clip it and surface the full value on
     hover via the `title` attribute. 90px fits the leading YYYY-MM-DD
     portion that operators actually care about. */
  td.collection-date {
    max-width: 90px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Generic "this column must not wrap" utility. Used on date/time
     columns in the Process Queue (Received, Runs at) so that long
     locale-formatted timestamps like "5/22/2026, 3:45:30 PM" don't
     break at the space and double the row height — which used to
     happen sporadically when a wide cooldown-bar (formerly 200px) in the
     adjacent column squeezed the timestamp column thin. */
  td.nowrap { white-space: nowrap; }
  td.dim { color: #888; font-size:14px; }

  /* -------- Filter bar -------- */
  .filterbar {
    display: flex; gap: 8px; align-items: center;
    margin-bottom: 12px;
    flex-wrap: wrap;
  }
  .filterbar select, .filterbar input {
    padding: 7px 10px;
    border: 1px solid #ccc;
    border-radius: 3px;
    font-size:15px;
    background: #fff;
    font-family: inherit;
  }
  /* Buttons inside a filterbar (Reset, Release Selected, Cancel
     Selected, Clear Selection) need to match the height of the
     input/select next to them — the global `button.small` (5px/13px)
     renders ~4px shorter than the inputs (7px/14px) and looks
     misaligned. Override here only; outside the filterbar (e.g. table
     row actions) `button.small` keeps its compact size. */
  .filterbar button,
  .filterbar button.small {
    padding: 7px 12px;
    font-size:15px;
    line-height: 1.2;
  }

  /* -------- Process Queue bulk-action toolbar (lives inside the
     Process Queue filterbar, to the right of the search input).
     Always visible. Buttons go disabled when no holds are selected.
     A small "N selected" badge appears between the buttons and the
     search box once any holds are ticked. */
  .bulk-count {
    display: inline-flex;
    align-items: center;
    padding: 0 10px;
    font-size:13px;
    color: #555;
    /* Hidden when no selection — keeps the row compact. */
    display: none;
  }
  .bulk-count.show { display: inline-flex; }
  .bulk-count b { color: #1976d2; margin-right: 4px; }

  /* -------- Badges -------- */
  .badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 3px;
    font-size:13px;
    font-weight: 500;
    border: 1px solid transparent;
    white-space: nowrap;
  }
  .badge.succeeded       { background: #e8f5e9; color: #2e7d32; border-color: #c8e6c9; }
  .badge.running         { background: #e3f2fd; color: #1565c0; border-color: #bbdefb; }
  .badge.queued          { background: #e3f2fd; color: #1565c0; border-color: #bbdefb; }
  .badge.waiting_cooldown{ background: #fff8e1; color: #ef6c00; border-color: #ffe0b2; }
  .badge.failed_retrying,
  .badge.temp_failed     { background: #fff8e1; color: #ef6c00; border-color: #ffe0b2; }
  .badge.failed_permanent{ background: #ffebee; color: #c62828; border-color: #ffcdd2; }
  .badge.failed_sync{ background: #fff3e0; color: #e65100; border-color: #ffcc80; }
  .badge.received        { background: #f5f5f5; color: #555; border-color: #ddd; }

  .svc {
    font-size:14px; padding: 1px 7px;
    border-radius: 3px; border: 1px solid #ddd;
    background: #f5f5f5; color: #333;
    /* Service names contain hyphens (Gen-Decoder, Omni-Health,
       Epi-Insight); without this the browser would break the chip
       at the hyphen when the column is squeezed. */
    white-space: nowrap;
  }

  /* External-link icon used inline next to identifier values in the
     drawer (pId → Admin Portal, OCR job_id → Report Manager).

     Visual goal: read as a button at a glance, not a decorative arrow.
     We use a permanent light-blue chip (background + 1px blue border)
     so the affordance is visible without hovering — operators have
     told us repeatedly that the bare-arrow version blends into the
     surrounding text. The inline SVG is the standard external-link
     glyph (box + arrow); it's sized at 14px which lines up cleanly
     with the 13px label-value text next to it.
     Hover deepens the background as click feedback. */
  .ext-link-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: 6px;
    width: 22px;
    height: 22px;
    padding: 0;
    color: #1565c0;
    background: #e3f2fd;
    border: 1px solid #90caf9;
    border-radius: 4px;
    text-decoration: none;
    vertical-align: -5px;
    cursor: pointer;
    transition: background 0.12s, border-color 0.12s;
  }
  .ext-link-icon:hover {
    background: #bbdefb;
    border-color: #64b5f6;
    color: #0d47a1;
  }
  .ext-link-icon svg {
    width: 14px;
    height: 14px;
    display: block;
  }

  /* -------- Buttons -------- */
  button, .btn {
    background: #fff;
    color: #333;
    border: 1px solid #bbb;
    padding: 7px 14px;
    border-radius: 3px;
    font-size:15px;
    cursor: pointer;
    font-family: inherit;
  }
  button:hover { background: #f0f0f0; }
  button.primary { background: #1976d2; color: #fff; border-color: #1976d2; }
  button.primary:hover { background: #1565c0; }
  button.warn { background: #fff; color: #ef6c00; border-color: #ffb74d; }
  button.warn:hover { background: #fff8e1; }
  button.danger { background: #fff; color: #c62828; border-color: #e57373; }
  button.danger:hover { background: #ffebee; }
  /* Blue counterpart of .warn / .danger — white background, blue text
     and border. Used for the Manual Add Job header button so it visually
     matches Restart but with blue (different action category). */
  button.accent { background: #fff; color: #1976d2; border-color: #90caf9; }
  button.accent:hover { background: #e3f2fd; }
  button.small { padding: 5px 10px; font-size:14px; }
  button:disabled { opacity: 0.5; cursor: not-allowed; }

  /* Inline spinner used by the Manual Add Job submit button while the
     server runs setup_patient_info for each hold (Firestore reads +
     filesystem writes — synchronous, can take a few seconds for a
     batch). The spinner is a 13px circle with a single-side accent
     border that rotates; sized to sit comfortably inside a button
     next to text without changing the button's height. */
  .spinner-inline {
    display: inline-block;
    width: 13px; height: 13px;
    border: 2px solid rgba(255,255,255,0.45);
    border-top-color: #fff;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    vertical-align: -2px;
    margin-right: 6px;
  }
  @keyframes spin { to { transform: rotate(360deg); } }

  /* -------- Cooldown progress bar -------- */
  .cooldown-bar {
    width: 90px; height: 8px;
    background: #eee; border-radius: 3px; overflow: hidden;
    margin-bottom: 4px;
  }
  .cooldown-bar-fill { height: 100%; background: #ffb74d; }

  /* -------- Drawer -------- */
  .drawer-backdrop {
    position: fixed; inset: 0;
    background: rgba(0,0,0,0.35);
    opacity: 0; pointer-events: none;
    transition: opacity 0.2s; z-index: 200;
  }
  .drawer-backdrop.open { opacity: 1; pointer-events: auto; }
  .drawer {
    position: fixed; top: 0; right: 0; bottom: 0;
    width: 85%; max-width: 1100px;
    background: #fff;
    border-left: 1px solid #ddd;
    transform: translateX(100%);
    transition: transform 0.22s ease-out;
    z-index: 201;
    display: flex; flex-direction: column;
    box-shadow: -4px 0 16px rgba(0,0,0,0.10);
  }
  .drawer.open { transform: translateX(0); }
  .drawer-header {
    padding: 16px 22px;
    border-bottom: 1px solid #eee;
    display: flex; justify-content: space-between; align-items: center;
    background: #fafafa;
  }
  .drawer-header h3 {
    margin: 0; font-size:16px; font-weight: 600;
    word-break: break-all;
  }
  .drawer-meta {
    padding: 12px 22px;
    border-bottom: 1px solid #eee;
    font-size:14px; color: #666;
    display: flex; gap: 24px; flex-wrap: wrap;
    background: #fafafa;
  }
  .drawer-meta b { color: #333; font-weight: 600; }
  .drawer-section {
    padding: 14px 22px;
    border-bottom: 1px solid #eee;
  }
  .drawer-section h4 {
    margin: 0 0 10px;
    font-size:13px; font-weight: 600; color: #777;
    text-transform: uppercase; letter-spacing: 0.05em;
  }
  .actions-row { display: flex; gap: 8px; flex-wrap: wrap; }

  /* -- attempt history rows are clickable to switch the log -- */
  .history-row {
    display: grid;
    grid-template-columns: 60px 150px 90px 1fr;
    gap: 12px;
    padding: 8px 10px;
    color: #666; font-size:14px;
    border: 1px solid transparent;
    border-radius: 3px;
    cursor: pointer;
    margin-bottom: 4px;
  }
  .history-row:hover { background: #f5f5f5; }
  .history-row.selected {
    background: #e3f2fd; border-color: #bbdefb;
  }
  .history-row b { color: #333; font-weight: 600; }
  .err-detail {
    background: #ffebee;
    color: #c62828;
    font-size:14px;
    font-family: ui-monospace, Menlo, Consolas, monospace;
    padding: 10px 12px;
    border-left: 3px solid #c62828;
    white-space: pre-wrap;
    margin: 6px 0 6px 70px;
    max-height: 160px; overflow: auto;
    border-radius: 0 3px 3px 0;
  }
  /* Cap the history section so a job with many failed attempts doesn't
     push the log region off-screen. The whole list scrolls inside this
     box; the log below is always visible. */
  #drawer-history {
    max-height: 300px;
    overflow: auto;
  }

  /* -- log area is the biggest piece -- */
  .drawer-body-wrap {
    flex: 1;
    display: flex; flex-direction: column;
    min-height: 200px;     /* hard floor: log is ALWAYS at least this tall */
  }
  .drawer-body-toolbar {
    padding: 10px 22px;
    background: #fafafa;
    border-bottom: 1px solid #eee;
    display: flex; align-items: center; gap: 10px;
    font-size:14px; color: #555;
  }
  .drawer-body {
    flex: 1; overflow: auto;
    padding: 16px 22px;
    font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size:14px; line-height: 1.55;
    white-space: pre-wrap;
    background: #1e1e1e;        /* dark log box */
    color: #e0e0e0;
    min-height: 200px;          /* visual floor */
  }

  /* -------- Toast -------- */
  .toast {
    position: fixed; bottom: 20px; left: 50%;
    transform: translateX(-50%) translateY(80px);
    background: #333; color: #fff;
    padding: 10px 18px;
    border-radius: 3px; font-size:15px;
    opacity: 0; transition: all 0.2s;
    z-index: 300;
  }
  .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
  .toast.err { background: #c62828; }
  .toast.ok { background: #2e7d32; }

  /* -------- Empty / loading -------- */
  .empty {
    text-align: center; color: #888;
    padding: 30px; font-size:15px;
  }

  /* -------- Connectivity row -------- */
  .conn-row {
    display: flex; gap: 12px; flex-wrap: wrap;
  }
  .conn-card {
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 14px 16px;
    min-width: 280px;
    flex: 1;
  }
  .conn-card .conn-title {
    font-weight: 600; margin-bottom: 6px; font-size:15px;
  }
  .conn-card .conn-status {
    font-size:14px; color: #666; margin-bottom: 10px;
  }
  .conn-card .conn-status.ok b { color: #2e7d32; }
  .conn-card .conn-status.fail b { color: #c62828; }

  /* -------- Runtime config editor -------- */
  .config-table {
    width: 100%; table-layout: fixed; border-collapse: collapse;
  }
  .config-table th, .config-table td {
    padding: 8px 10px; vertical-align: top;
    border-bottom: 1px solid #f0f0f0;
    /* Long values (e.g. email recipient lists, long descriptions) must
       wrap inside the cell instead of forcing the modal wider. */
    word-break: break-word; overflow-wrap: anywhere;
  }
  .config-table input[type=number], .config-table input[type=text] {
    padding: 5px 8px; font-size:15px;
    border: 1px solid #ccc; border-radius: 3px;
    width: 110px;
    font-family: inherit;
    box-sizing: border-box;
    max-width: 100%;
  }
  .restart-banner {
    background: #fff8e1; border: 1px solid #ffe0b2;
    color: #ef6c00;
    padding: 10px 14px;
    border-radius: 3px;
    margin-bottom: 12px;
    font-size:15px;
    display: none;
  }
  .restart-banner.show { display: block; }

  /* -------- Modal for Runtime Config -------- */
  .modal-backdrop {
    position: fixed; inset: 0;
    background: rgba(0,0,0,0.45);
    opacity: 0; pointer-events: none;
    transition: opacity 0.2s; z-index: 250;
  }
  .modal-backdrop.open { opacity: 1; pointer-events: auto; }
  .modal {
    position: fixed; top: 50%; left: 50%;
    transform: translate(-50%, -50%) scale(0.96);
    background: #fff; border-radius: 6px;
    width: 90%; max-width: 900px;
    max-height: 90vh;
    /* Vertical scroll only. Horizontal overflow is prevented; long content
       (error messages, doc ids, etc.) must wrap inside cells -- see
       .state-jobs-table and .config-table for the per-cell wrap rules. */
    overflow-x: hidden; overflow-y: auto;
    box-shadow: 0 20px 60px rgba(0,0,0,0.25);
    opacity: 0; pointer-events: none;
    transition: all 0.2s; z-index: 251;
  }
  .modal.open { opacity: 1; transform: translate(-50%, -50%) scale(1); pointer-events: auto; }
  .modal-header {
    padding: 14px 20px; border-bottom: 1px solid #eee;
    display: flex; justify-content: space-between; align-items: center;
  }
  .modal-header h3 { margin: 0; font-size:17px; font-weight: 600; }
  .modal-body { padding: 16px 20px; }

  /* -------- Custom dialog (confirm / alert replacement) --------
     Replaces window.confirm / window.alert with a dashboard-styled
     modal. Uses the existing .modal and .modal-backdrop machinery
     (#dialog-backdrop / #dialog-modal in HTML, opened by showConfirm /
     showAlert in JS). Smaller than the regular modals because the
     payload is just a short message + 1-2 buttons. */
  .modal.dialog {
    max-width: 460px;
    /* Dialogs sit above other modals (Help, Add Hold, Config, etc.)
       so an action triggered from inside one of those still gets
       confirmation visible on top. */
    z-index: 261;
  }
  .modal-backdrop.dialog { z-index: 260; }
  .dialog-message {
    padding: 18px 20px;
    font-size:15px;
    line-height: 1.55;
    color: #333;
    /* preserve newlines from the message string so the existing
       "title\n\ndetail" pattern from the old confirm() calls renders
       sensibly without rewriting every call site. */
    white-space: pre-wrap;
    word-break: break-word;
  }
  /* When the dialog is the destructive / dangerous variant, give the
     message a faint red bar on the left to flag it before reading. */
  .modal.dialog.danger .dialog-message {
    border-left: 3px solid #c62828;
    background: #fff8f8;
  }
  .modal.dialog.warn .dialog-message {
    border-left: 3px solid #ef6c00;
    background: #fffaf2;
  }
  /* Detail block — shown below the main message, styled like a code
     block. Used for multi-line failure listings (e.g. bulk-release
     partial failures). */
  .dialog-detail {
    /* Top margin keeps a visible gap between the (possibly tinted)
       message block above and this code-style box. */
    margin: 10px 20px 14px;
    padding: 10px 12px;
    background: #fafafa;
    border: 1px solid #e0e0e0;
    border-radius: 3px;
    font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size:13.5px;
    color: #555;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 240px;
    overflow: auto;
  }
  .dialog-footer {
    padding: 12px 20px 16px;
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    border-top: 1px solid #f0f0f0;
    background: #fafafa;
    border-radius: 0 0 6px 6px;
  }

  /* Spinner shown inside the OK button while the dialog's onConfirm
     callback is running (pull / hard pull). 14×14 keeps it the same
     visual height as the button text it replaces, so the button
     doesn't reflow. We render it via inline-block + animation rather
     than an SVG so it inherits the button's currentColor. */
  .dialog-spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: dialog-spin 0.7s linear infinite;
    vertical-align: -2px;
  }
  @keyframes dialog-spin { to { transform: rotate(360deg); } }
  /* Check mark shown briefly between "spinner" and "modal closes" so
     the operator sees an unambiguous success signal. Matches the
     button's currentColor; bold so it reads clearly at button size. */
  .dialog-checkmark {
    display: inline-block;
    font-weight: 700;
    font-size: 16px;
    line-height: 1;
    vertical-align: -1px;
  }
  /* X mark — symmetric counterpart to .dialog-checkmark. Used to flag
     a failed async operation in the same place the spinner was: the
     button briefly shows × before the dialog closes and a follow-up
     error alert opens. Same currentColor inheritance so it reads
     correctly on primary (white) and danger (red) buttons. */
  .dialog-xmark {
    display: inline-block;
    font-weight: 700;
    font-size: 16px;
    line-height: 1;
    vertical-align: -1px;
  }

  /* -------- Job counts: standalone clickable cards (own section) -------- */
  /* These were inline in the System status row — pulled out so the four
     counts (Running / Queued / Cooldown / Permanent fails) get their own
     attention. Click opens a modal listing every job in that state. */
  .jobs-counts-row {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 12px;
  }
  /* Below 1100px (typical laptops in split-screen) 5 cards get cramped.
     Drop to 3 columns; below 700px (tablet portrait / phone) drop to 2. */
  @media (max-width: 1100px) {
    .jobs-counts-row { grid-template-columns: repeat(3, 1fr); }
  }
  @media (max-width: 700px) {
    .jobs-counts-row { grid-template-columns: repeat(2, 1fr); }
  }
  .jobs-count-card {
    background: #fff; border: 1px solid #ddd; border-radius: 4px;
    padding: 18px 20px;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, transform 0.1s;
    display: flex; flex-direction: column; gap: 4px;
  }
  .jobs-count-card:hover {
    background: #f5faff; border-color: #1976d2;
    transform: translateY(-1px);
  }
  /* Read-only cards: no hover effect, no pointer cursor. Used for the
     Successful card which is informational only. */
  .jobs-count-card.not-clickable {
    cursor: default;
  }
  .jobs-count-card.not-clickable:hover {
    background: #fff; border-color: #ddd;
    transform: none;
  }
  .jobs-count-card .label {
    font-size:13px; text-transform: uppercase; color: #666;
    letter-spacing: 0.04em; font-weight: 600;
  }
  .jobs-count-card .value {
    font-size: 28px; font-weight: 700; color: #222;
    font-variant-numeric: tabular-nums;
  }
  .jobs-count-card .hint {
    font-size:12px; color: #999;
  }
  .jobs-count-card.warn .value { color: #ef6c00; }
  .jobs-count-card.err .value { color: #c62828; }
  .jobs-count-card.err { border-color: #ffcdd2; }

  /* -------- Sortable column headers (Recent jobs + Process Queue) -------- */
  /* Just the cursor + a ⇅ glyph after the label so the user knows it's
     clickable. No background changes — keep the table header looking
     normal. When sort is active, JS swaps the glyph to ↑ or ↓. */
  th.sortable {
    cursor: pointer;
    user-select: none;
    /* Keep the label and the ⇅/↑/↓ indicator together on one line —
       without this, narrow columns like pId let the indicator wrap
       under the label. */
    white-space: nowrap;
  }
  .sort-indicator {
    display: inline-block;
    margin-left: 4px;
    font-size: 11px;
    min-width: 12px;
    color: #1976d2;
    font-weight: 600;
  }
  .sort-indicator:empty::before {
    content: "⇅";
    color: #b0bec5;
    font-weight: 400;
  }

  /* -------- Pagination controls (Recent jobs) -------- */
  .pager {
    display: flex; align-items: center; gap: 6px;
    margin-top: 12px;
    font-size:14px; color: #666;
  }
  .pager button {
    background: #fff; border: 1px solid #ccc;
    padding: 4px 10px; border-radius: 3px; cursor: pointer;
    font-size:14px;
    color: #333;
  }
  .pager button:hover:not(:disabled) { background: #f0f0f0; }
  .pager button:disabled {
    color: #aaa; cursor: not-allowed; background: #fafafa;
  }
  .pager .page-input {
    width: 60px; padding: 4px 6px;
    border: 1px solid #ccc; border-radius: 3px;
    text-align: center;
    font-family: inherit; font-size:14px;
  }
  .pager .total-info {
    margin-left: auto; color: #888; font-size:13px;
  }
  /* Per-page selector inside the pager. Sits to the right of total-info
     (which is margin-left:auto, so it gets pushed all the way right;
     this select then sits flush against it via a small gap from the
     parent flex container). */
  .pager .pager-perpage {
    margin-left: 12px;
    padding: 4px 8px; font-size:14px;
    border: 1px solid #ccc; border-radius: 3px;
    background: #fff; color: #333;
    font-family: inherit;
    cursor: pointer;
  }

  /* -------- Tabbed modal (Help, jobs detail) -------- */
  .modal-tabs {
    display: flex; gap: 0;
    border-bottom: 1px solid #eee;
    padding: 0 20px;
    background: #fafafa;
  }
  .modal-tabs button {
    background: transparent; border: 0;
    padding: 12px 18px;
    font-size:15px; color: #666;
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
  }
  .modal-tabs button:hover { color: #333; background: rgba(0,0,0,0.03); }
  .modal-tabs button.active {
    color: #1976d2; border-bottom-color: #1976d2;
    font-weight: 600;
  }
  .modal-tab-pane { display: none; }
  .modal-tab-pane.active { display: block; }
  .modal-tab-pane h4 {
    margin: 18px 0 8px; font-size:15px; color: #444;
  }
  .modal-tab-pane h4:first-child { margin-top: 0; }
  .modal-tab-pane p {
    margin: 6px 0 12px; line-height: 1.65; color: #444;
  }
  .modal-tab-pane code {
    background: #f5f5f5; padding: 1px 6px; border-radius: 3px;
    font-size:13.5px;
  }
  .modal-tab-pane ul { padding-left: 22px; line-height: 1.8; }
  .modal-tab-pane .term {
    color: #1976d2; font-weight: 600;
  }
  /* Larger Help modal */
  .modal.wide { max-width: 1040px; }

  /* Jobs-by-state modal: simple table */
  .state-jobs-table {
    width: 100%; border-collapse: collapse; font-size:14px;
    /* Cap width to the modal body so an oversized cell can't widen the
       table past the modal. Combined with word-break below, long error
       messages / doc ids wrap inside their cell. */
    table-layout: auto; max-width: 100%;
  }
  .state-jobs-table th, .state-jobs-table td {
    padding: 7px 10px; text-align: left;
    border-bottom: 1px solid #eee;
    /* Long error messages (Permanent failures) and long doc ids must
       wrap inside the cell rather than force horizontal scroll on the
       modal. */
    word-break: break-word; overflow-wrap: anywhere;
  }
  .state-jobs-table th {
    background: #fafafa; font-weight: 600;
    color: #555; font-size:13px;
    text-transform: uppercase; letter-spacing: 0.03em;
  }
  .state-jobs-table tr.row-link { cursor: pointer; }
  .state-jobs-table tr.row-link:hover { background: #f5faff; }
  .state-jobs-table .doc-id {
    font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size:13px;
  }
  /* Long columns (doc id / patient id / timestamp) get single-line
     ellipsis instead of wrapping, so the table stays compact and rows
     line up. The cell carries a native title="<full>" so hovering shows
     the full value (the whole row is clickable, so a click-popover would
     conflict). The Error column intentionally keeps the wrapping rule
     above so failure messages stay readable. */
  .state-jobs-table td.trunc-col {
    max-width: 180px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* override the table-wide word-break/overflow-wrap so ellipsis wins */
    word-break: normal; overflow-wrap: normal;
  }

  /* Sub-page tab bar (e.g. /jobs uses this to switch between Recent
     Jobs and Process Queue). Visually mirrors .modal-tabs but lives
     outside a modal — separate selector to keep modal styling
     unchanged. */
  .page-tabs {
    display: flex; gap: 0;
    border-bottom: 1px solid #eee;
    margin: 18px 0 24px;
    align-items: center;
  }
  .page-tabs button.tab-btn {
    background: transparent; border: 0;
    padding: 12px 18px;
    font-size:15px; color: #666;
    cursor: pointer;
    border-bottom: 2px solid transparent;
    font-family: inherit;
  }
  .page-tabs button.tab-btn:hover { color: #333; background: rgba(0,0,0,0.03); }
  .page-tabs button.tab-btn.active {
    color: #1976d2; border-bottom-color: #1976d2;
    font-weight: 600;
  }
  .page-tabs .page-refresh-btn {
    margin-left: auto;
    background: #fff;
    border: 1px solid #ccc;
    padding: 6px 14px;
    border-radius: 3px;
    font-size: 13px; color: #333;
    cursor: pointer;
    font-family: inherit;
  }
  .page-tabs .page-refresh-btn:hover { background: #f5f5f5; }
  .page-tab-pane { display: none; }
  .page-tab-pane.active { display: block; }
