/* ==========================================================================
   ParticleLabeler - feature styles
   Page-specific composition on top of app.css. Depends on tokens.css.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Batches
   -------------------------------------------------------------------------- */
.batch-list-table td:first-child { font-weight: var(--weight-medium); color: var(--text-strong); }
.batch-list-detail, .batch-list-expanded-row { background: var(--surface-sunken); }
/* The figures and the metadata are two separate readings, and nothing said so:
   the block had no layout at all, so the panel heading sat directly against the
   tiles above it. */
.batch-list-detail {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}
.batch-list-expanded-row > td { padding: var(--space-4) var(--space-5); }
.expanded-loading { padding: var(--space-4); color: var(--text-muted); font-size: var(--text-sm); }

.profile-details {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--space-3) var(--space-5);
}
.profile-details > div { display: flex; flex-direction: column; gap: 2px; }
.profile-details .label {
    font-size: var(--text-xs); font-weight: var(--weight-semibold);
    letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted);
}
.profile-details .value { margin: 0; font-size: var(--text-sm); color: var(--text-strong); }

.batch-metadata, .batch-list-metadata { display: flex; flex-direction: column; gap: var(--space-4); }
.batch-metadata-group, .batch-list-metadata-group, .metadata-group { display: flex; flex-direction: column; gap: var(--space-2); }
/* h4 as well as h3: the expanded row nests one level deeper and headed its
   groups with h4, which this rule never matched - so the same headings were
   styled on the detail page and left as bare bold text in the list. */
.batch-metadata-group > :is(h3, h4),
.batch-list-metadata-group > :is(h3, h4) {
    font-size: var(--text-xs); font-weight: var(--weight-semibold);
    letter-spacing: .07em; text-transform: uppercase; color: var(--pm-orange-700);
    padding-bottom: var(--space-1); border-bottom: 1px solid var(--border-subtle);
}
.batch-metadata-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: var(--space-2) var(--space-4);
    align-items: start;
}
.batch-metadata-grid > label {
    display: grid;
    grid-template-rows: auto auto auto;
    gap: var(--space-1);
    align-content: start;
    min-width: 0;
}
@supports (grid-template-rows: subgrid) {
    .batch-metadata-grid > label {
        grid-row: span 3;
        grid-template-rows: subgrid;
    }
}
.batch-metadata-summary { font-size: var(--text-sm); color: var(--text-muted); }

.batch-form, .metadata-form, .review-metadata-form, .user-create-form, .password-form {
    display: flex; flex-direction: column; gap: var(--space-5);
}
.batch-basics { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space-3) var(--space-4); }
.batch-basics > h3 { grid-column: 1 / -1; }
.batch-basics .wide-field { grid-column: 1 / -1; }

/* Section headings inside a form: quieter than a page heading, still a clear break. */
.batch-form-section > h3, .metadata-form > h3 {
    font-size: var(--text-xs); font-weight: var(--weight-semibold);
    letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted);
}
.metadata-group > h4 {
    font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-strong);
    padding-bottom: var(--space-1); border-bottom: 1px solid var(--border-subtle);
}
.batch-form-section { display: flex; flex-direction: column; gap: var(--space-3); padding-top: var(--space-4); border-top: 1px solid var(--border-subtle); }
.batch-form-section:first-child { padding-top: 0; border-top: 0; }

/* --------------------------------------------------------------------------
   Import and device settings
   -------------------------------------------------------------------------- */
.import-panel { display: flex; flex-direction: column; gap: var(--space-4); }
.upload-picker { display: flex; flex-direction: column; gap: var(--space-3); }
.import-table td { font-variant-numeric: tabular-nums; }
.job-progress-track { width: 140px; }

.device-settings-panel {
    display: flex; flex-direction: column; gap: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
}
/* auto-fill rather than auto-fit: with three suggestions the tiles keep their width
   instead of stretching a two-word value across a third of the panel. */
.device-settings-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--space-3) var(--space-5);
    margin: 0;
    font-size: var(--text-sm);
}
.device-settings-list > div { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.device-settings-list dt {
    font-size: var(--text-xs); font-weight: var(--weight-semibold);
    letter-spacing: .04em; text-transform: uppercase; color: var(--text-muted);
}
.device-settings-list dd { margin: 0; display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.device-settings-list .device-settings-value {
    font-size: var(--text-md); font-weight: var(--weight-medium);
    color: var(--text-strong); font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}
/* Where the value came from, kept legible but plainly secondary. */
.device-settings-list dd small {
    font-size: var(--text-xs); color: var(--text-subtle);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Metadata catalogue administration
   -------------------------------------------------------------------------- */
.metadata-table td:first-child { font-weight: var(--weight-medium); }

.option-rules { display: flex; flex-direction: column; gap: var(--space-2); }
.option-rule-header, .option-rule-row {
    display: grid;
    gap: var(--space-2);
    align-items: center;
}
/* Two shapes, because the particle-size assignment adds a target field and a value.
   Declaring only the short one let the extra cells wrap onto invented rows. */
.option-rule-header.without-auto-fill,
.option-rule-row.without-auto-fill {
    grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr) minmax(0, 1.2fr) auto;
}
.option-rule-header.with-auto-fill,
.option-rule-row.with-auto-fill {
    grid-template-columns: minmax(0, .7fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, .8fr) auto;
}
.option-rule-header { font-size: var(--text-xs); font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); }
.field-auto-fill { font-size: var(--text-xs); color: var(--pm-navy-600); }
.with-auto-fill { border-left: 2px solid var(--pm-navy-500); padding-left: var(--space-2); }
.without-auto-fill { color: var(--text-muted); }

/* --------------------------------------------------------------------------
   PSF calibration
   -------------------------------------------------------------------------- */

/* One column, like the labeling viewer: heading, tools, then the frame across the full
   width. As a two-column grid the children fell into it in reading order, which put the
   review block beside the image and squeezed the frame into a third of the panel. */
.psf-fit-panel { display: flex; flex-direction: column; gap: var(--space-4); }
.psf-fit-panel > .psf-image-stage { width: 100%; }

/* Same shape as .profile-facts: a quiet uppercase label, the value beneath it. The tiles
   carried no layout at all before, so label and value ran together with no gap. */
.psf-summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--space-3) var(--space-5);
}
.psf-summary-grid > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.psf-summary-grid > div > span {
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.psf-summary-grid > div > strong {
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    color: var(--text-strong);
    font-variant-numeric: tabular-nums;
}
.psf-profile-summary { display: flex; flex-direction: column; gap: var(--space-2); font-size: var(--text-sm); }
.psf-readiness-hint, .psf-detection-diagnostics, .psf-fitting-status { font-size: var(--text-xs); color: var(--text-muted); }
.psf-empty-panel { display: grid; place-items: center; min-height: 220px; color: var(--text-muted); }
.psf-canvas-actions, .psf-review-actions {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-4); flex-wrap: wrap;
    padding: var(--space-3) 0;
}
.psf-review-actions > div:first-child { display: flex; flex-direction: column; gap: 2px; font-size: var(--text-sm); }
.psf-review-actions strong { color: var(--text-strong); }
.psf-snr-setting { display: block; margin: var(--space-3) 0; }

.psf-fits-details { border-top: 1px solid var(--border-subtle); padding-top: var(--space-3); margin-top: var(--space-3); }
.psf-fits-details summary { cursor: pointer; font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-link); }
.psf-fits-details-content { margin-top: var(--space-3); max-height: 340px; overflow: auto; }
.psf-fit-table td { font-variant-numeric: tabular-nums; }

.psf-vector-overlay { position: absolute; inset: 0; pointer-events: none; }
/* The geometry scales with the image - a search box is a region of the frame, as in
   PixInsight - but the lines stay hairline thin at every zoom. non-scaling-stroke cannot
   do that here: the scaling comes from a CSS transform on an ancestor, which it does not
   see. Dividing by the published zoom factor does. */
/* Width and colour come from the viewer's own settings; the division by the published zoom
   is what keeps a marker a hairline however far in you are. --marker-stroke is unitless so
   it can be multiplied here rather than needing a second variable per zoom level. */
.psf-vector-fit, .psf-fit-ellipse {
    fill: none;
    stroke: var(--marker-fit);
    stroke-width: calc(var(--marker-stroke, 1) * 1px / var(--zoom, 1));
}
/* The dashed ellipse IS the fit: its axes are FWHMx and FWHMy, so its shape shows the
   roundness and its rotation the fitted angle - the same reading as in DynamicPSF. Dash and
   gap arrive as two separate values because each has to be divided by the zoom on its own;
   a solid stroke is simply a dash with no gap, so this rule needs no special case. */
.fit-ellipse {
    fill: none;
    stroke: var(--marker-fit);
    stroke-width: calc(var(--marker-stroke, 1) * 1px / var(--zoom, 1));
    stroke-dasharray:
        calc(var(--marker-psf-ellipse-dash, 3) * 1px / var(--zoom, 1))
        calc(var(--marker-psf-ellipse-gap, 2.5) * 1px / var(--zoom, 1));
}
/* Same reasoning as above: hairline strokes, divided by the published zoom factor. */
.psf-vector-detection, .detection-circle {
    fill: none;
    stroke: var(--marker-detection);
    stroke-width: calc(var(--marker-stroke, 1) * 1px / var(--zoom, 1));
}
.detection-line, .fit-center-line {
    stroke: var(--marker-psf-centre, var(--marker-detection));
    stroke-width: calc(var(--marker-stroke, 1) * 1px / var(--zoom, 1));
}
/* An open ring, not a filled disc. A fill scales with the image and turns into a blob
   that covers the very centre it marks - which is the one place that has to stay visible. */
.fit-center-circle {
    fill: none;
    stroke: var(--marker-psf-centre, var(--marker-selected));
    stroke-width: calc(var(--marker-stroke, 1) * 1px / var(--zoom, 1));
}
/* The search region the fit was taken from. It used to carry a hardcoded colour - the only
   one of these that could not follow the viewer's settings. */
.fit-window, .psf-fit-window {
    fill: none;
    stroke: var(--marker-psf-box, var(--marker-selected));
    stroke-width: calc(var(--marker-stroke, 1) * 1px / var(--zoom, 1));
    stroke-dasharray:
        calc(var(--marker-psf-box-dash, 1) * 1px / var(--zoom, 1))
        calc(var(--marker-psf-box-gap, 0) * 1px / var(--zoom, 1));
}
.psf-fit-marker {
    fill: none;
    stroke: var(--marker-fit);
    stroke-width: calc(var(--marker-stroke, 1) * 1px / var(--zoom, 1));
}
/* Not the sample under examination. Colour then means "this is the one", as in DynamicPSF,
   instead of every marker shouting at once. */
.psf-vector-overlay .is-idle,
.psf-vector-overlay .is-idle * {
    stroke: var(--marker-psf-idle, #9aa7b0);
}

/* --------------------------------------------------------------------------
   Preannotation workbench
   -------------------------------------------------------------------------- */
.preannotation-workbench { display: grid; grid-template-columns: minmax(0, 1fr) minmax(290px, 390px); gap: var(--space-5); align-items: start; }
@media (max-width: 1180px) { .preannotation-workbench { grid-template-columns: minmax(0, 1fr); } }
.preannotation-side-panel { display: flex; flex-direction: column; gap: var(--space-5); position: sticky; top: calc(var(--brandbar-height) + var(--space-4)); }
.preannotation-side-panel > section { display: flex; flex-direction: column; gap: var(--space-2); }

.preannotation-algorithms { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--space-3); }
.preannotation-algorithm {
    display: flex; align-items: flex-start; gap: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    text-align: left;
    cursor: pointer;
    transition: border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out);
}
.preannotation-algorithm:hover { border-color: var(--pm-orange-400); background: var(--surface-hover); }
.preannotation-algorithm.selected { border-color: var(--pm-orange-500); box-shadow: 0 0 0 1px var(--pm-orange-500); background: var(--pm-orange-100); }
.preannotation-algorithm > span:last-child { display: flex; flex-direction: column; gap: var(--space-1); }
.preannotation-algorithm strong { font-size: var(--text-base); color: var(--text-strong); }
.preannotation-algorithm small { font-size: var(--text-xs); line-height: var(--leading-snug); color: var(--text-muted); }
.preannotation-algorithm-icon { display: grid; place-items: center; width: 36px; height: 36px; flex: 0 0 36px; border-radius: var(--radius-md); background: var(--info-100); }
.preannotation-algorithm-icon svg { width: 20px; height: 20px; stroke: var(--pm-navy-600); fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

.preannotation-settings { display: flex; flex-direction: column; gap: var(--space-3); }
.preannotation-settings label { display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--text-sm); font-weight: var(--weight-medium); }
.preannotation-settings .checkbox-label { flex-direction: row; align-items: center; }
.preannotation-run-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: var(--space-3); font-size: var(--text-sm); }
/* The frame chooser is a strip, not a column: it is picked from once and then wants to be
   out of the way, so it wraps across the full width and scrolls after a few rows. */
.preannotation-image-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    max-height: 148px;
    overflow: hidden auto;
    padding: 2px;
    scrollbar-width: thin;
}
.preannotation-image-list button {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-sm);
    background: var(--surface-raised);
    font: inherit;
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    color: var(--text-default);
    white-space: nowrap;
    cursor: pointer;
}
.preannotation-image-list button small { color: var(--text-muted); }
.preannotation-image-list button:hover { border-color: var(--border-strong); background: var(--surface-hover); }
.preannotation-image-list button.is-selected { border-color: var(--pm-orange-500); background: var(--pm-orange-100); color: var(--text-strong); }
.preannotation-image-list button.is-selected small { color: inherit; }

/* Three regions, not two. This grid had three children but only two columns, so the
   coordinate list wrapped underneath the frame chooser and the viewer - the thing the
   dialog exists for - was left with the 320px sidebar. */
.detection-results-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
    grid-template-areas:
        "images images"
        "viewer list";
    gap: var(--space-4);
    align-items: start;
}
.detection-results-layout > .preannotation-image-list { grid-area: images; }
.detection-results-layout > .detection-viewer { grid-area: viewer; min-width: 0; }
.detection-results-layout > .detection-coordinate-list { grid-area: list; }
@media (max-width: 1080px) {
    .detection-results-layout {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "images" "viewer" "list";
    }
}

.detection-coordinate-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-height: min(62vh, 620px);
    overflow: hidden auto;
    scrollbar-width: thin;
}

/* Shares the item grid so the columns line up, and stays put while the list scrolls. The
   opaque background matters: without it the rows show through as they pass underneath. */
.detection-coordinate-head {
    position: sticky;
    top: 0;
    z-index: 1;
    display: grid;
    grid-template-columns: 2.75rem minmax(0, 1fr) auto;
    column-gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    margin-bottom: 2px;
    border-bottom: 1px solid var(--border-default);
    background: var(--surface-raised);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.detection-coordinate-head span:last-child { justify-self: end; }
/* Laid out in columns rather than pushed apart: space-between spread four items across the
   full width of the panel, so an index, a coordinate pair and a verdict ended up reading as
   three unrelated columns with a void between them. */
.detection-coordinate-item {
    display: grid;
    grid-template-columns: 2.75rem minmax(0, 1fr) auto;
    grid-template-areas:
        "index coordinates verdict"
        "index reason      reason";
    column-gap: var(--space-3);
    align-items: baseline;
    padding: var(--space-2) var(--space-3);
    border: 1px solid transparent;
    /* The accent carries the marker colour of the cross this row belongs to. */
    border-left: 3px solid transparent;
    border-radius: var(--radius-sm);
    background: none;
    font: inherit;
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    cursor: pointer;
    text-align: left;
}
.detection-coordinate-item > strong { grid-area: index; color: var(--text-muted); font-weight: var(--weight-medium); }
.detection-coordinate-item > code { grid-area: coordinates; font-family: var(--font-mono); color: var(--text-default); }
.detection-coordinate-item > small { grid-area: verdict; justify-self: end; white-space: nowrap; }
.detection-coordinate-item > small.field-help { grid-area: reason; justify-self: start; }
/* Clicking a cross scrolls its row into view with block:"nearest", which would park it
   underneath the sticky header. */
.detection-coordinate-item { scroll-margin-block: 44px; }
.detection-coordinate-item:hover { background: var(--surface-hover); }
.detection-coordinate-item.is-accepted { border-left-color: var(--marker-detection); }
.detection-coordinate-item.is-uncertain { border-left-color: var(--marker-uncertain); }

/* The selected row has to be findable at a glance after the view jumps to it, so it takes
   the selected-marker colour rather than a neutral grey - the same colour the cross on the
   frame switches to. */
.detection-coordinate-item.is-selected {
    border-color: var(--marker-selected);
    border-left-color: var(--marker-selected);
    background: color-mix(in srgb, var(--marker-selected) 26%, var(--surface-raised));
    color: var(--text-strong);
    font-weight: var(--weight-medium);
}
.detection-coordinate-item.is-selected > strong,
.detection-coordinate-item.is-selected > code,
.detection-coordinate-item.is-selected > small { color: var(--text-strong); }
.detection-coordinate-item.rejected { color: var(--danger-600); }

.image-stretch-controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--space-3); }

/* --------------------------------------------------------------------------
   Image modal / navigation
   -------------------------------------------------------------------------- */
.image-modal { width: min(1200px, 100%); padding: 0; gap: 0; }
.image-modal-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border-subtle); }
.image-modal-canvas { position: relative; background: var(--neutral-900); min-height: 420px; }
.image-nav-button {
    position: absolute; top: 50%; transform: translateY(-50%);
    display: grid; place-items: center;
    width: 40px; height: 40px;
    border: 0; border-radius: var(--radius-pill);
    background: rgba(12, 18, 26, .72); color: #fff;
    cursor: pointer;
}
.image-nav-button:hover { background: rgba(12, 18, 26, .92); }
.image-nav-previous { left: var(--space-3); }
.image-nav-next { right: var(--space-3); }

/* --------------------------------------------------------------------------
   Users
   -------------------------------------------------------------------------- */
.user-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.table-action-empty { color: var(--text-subtle); font-size: var(--text-xs); }
.language-switcher { display: flex; gap: var(--space-1); padding: 2px; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--surface-sunken); }
.language-switcher button {
    padding: 2px var(--space-2);
    border: 0; border-radius: var(--radius-sm);
    background: none; color: var(--text-muted);
    font: inherit; font-size: var(--text-xs); font-weight: var(--weight-semibold);
    cursor: pointer;
}
.language-switcher button.active { background: var(--surface-raised); color: var(--text-strong); box-shadow: var(--shadow-xs); }

.background-work-indicator { display: inline-flex; align-items: center; gap: var(--space-2); }
.background-work-indicator span:first-child {
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--pm-orange-500);
    animation: pulse 1.6s var(--ease-in-out) infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* --------------------------------------------------------------------------
   Remaining page classes
   These are used by pages that have not been rewritten yet. They are styled
   here rather than left unstyled so no screen regresses while the migration
   to the new component vocabulary continues.
   -------------------------------------------------------------------------- */
.secondary-button {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
    padding: var(--space-2) var(--space-4); min-height: 34px;
    border: 1px solid var(--border-default); border-radius: var(--radius-md);
    background: var(--surface-raised); color: var(--text-default);
    font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium);
    cursor: pointer;
    transition: background var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
}
.secondary-button:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--border-strong); }
.secondary-button:disabled { opacity: .5; cursor: not-allowed; }

.panel-heading-row {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-4);
}

.batch-list-panel, .image-preview-panel, .psf-run-panel { display: flex; flex-direction: column; gap: var(--space-4); }
.psf-cancel-note { font-size: var(--text-sm); color: var(--text-muted); }

/* Login and registration: labels sit directly above their input. */
.auth-form label { font-size: var(--text-sm); font-weight: var(--weight-medium); margin-bottom: var(--space-1); }
.auth-form label + input, .auth-form label + .password-input { margin-bottom: var(--space-3); }

/* Die Code-Eingabe. Gross, mittig und mit weiten Zeichenabstaenden: sechs Ziffern, die man aus
   einer Mail abtippt, will man beim Tippen lesen koennen, und Ziffern in Tabellenbreite
   springen dabei nicht hin und her. */
.auth-code {
    text-align: center;
    font-size: var(--text-2xl);
    letter-spacing: .45em;
    /* Der Abstand haengt rechts am letzten Zeichen und schoebe den Text sonst aus der Mitte. */
    text-indent: .45em;
    font-variant-numeric: tabular-nums;
    font-weight: var(--weight-semibold);
}

.auth-check {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    /* Der Rahmen setzt fuer jedes label einen Abstand nach unten - hier stoert er, weil der
       erklaerende Satz direkt darunter gehoert. */
    margin-bottom: 0;
    font-weight: var(--weight-regular);
    cursor: pointer;
}
.auth-check input { width: auto; margin: 0; }

.auth-hint {
    font-size: var(--text-xs);
    color: var(--text-muted);
    margin-top: calc(var(--space-2) * -1);
}

/* --------------------------------------------------------------------------
   Create-batch dialog
   The form is long. Header and actions stay put, only the fields scroll, and
   nothing may scroll sideways.
   -------------------------------------------------------------------------- */
.batch-create-modal {
    width: min(820px, 100%);
    max-height: min(88vh, 940px);
    padding: 0;
    gap: 0;
    overflow: hidden;
}
.batch-create-modal > header {
    display: flex; flex-direction: column; gap: var(--space-1);
    padding: var(--space-5) var(--space-5) var(--space-4);
    border-bottom: 1px solid var(--border-subtle);
}
.batch-create-modal > header p:last-child { font-size: var(--text-sm); color: var(--text-muted); }
.batch-create-modal .batch-form {
    min-height: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}
.batch-create-modal .batch-form > .batch-form-section { padding: var(--space-5); overflow-x: hidden; }
.batch-create-modal .batch-form > .batch-metadata {
    flex: 1;
    min-height: 0;
    overflow: hidden auto;
    padding-top: var(--space-4);
}
.batch-create-modal .batch-form > footer {
    display: flex; justify-content: flex-end; gap: var(--space-2);
    padding: var(--space-4) var(--space-5);
    border-top: 1px solid var(--border-subtle);
    background: var(--surface-sunken);
}

/* --------------------------------------------------------------------------
   Dense administration tables
   A row whose action buttons wrap onto three lines turns a 20-row list into a
   scrolling wall. Actions stay on one line, the column shrinks to its content,
   and everything else gets the remaining width.
   -------------------------------------------------------------------------- */
.data-table td { padding: var(--space-2) var(--space-3); }
.data-table th { padding: var(--space-2) var(--space-3); }

/* The actions column, kept to its own width and pushed to the right edge.
   The exclusion matters: a full-width detail row is a single cell, so it is its
   row's last child too, and without it that cell inherited the actions column's
   right alignment and 1% width - which squeezed a whole metadata panel into a
   right-hand sliver with the page's width sitting empty beside it.
   Wrapped in :where() so it adds no specificity. Written as a plain :not([colspan])
   it outweighed ".user-table td:last-child", which had until then won the tie on
   source order - and the user table's actions column silently dropped from its own
   340px to one percent of the table, thirteen pixels, with its buttons hanging out
   over the column beside them. */
.data-table td:last-child:not(:where([colspan])),
.data-table th:last-child:not(:where([colspan])) { width: 1%; white-space: nowrap; text-align: right; }

td .table-actions {
    flex-wrap: nowrap;
    justify-content: flex-end;
    gap: var(--space-1);
}
td .table-action-button {
    min-height: 26px;
    padding: var(--space-1) var(--space-2);
    font-size: var(--text-xs);
}

/* The German name leads, the English one is a quiet second line. */
.metadata-table td:first-child { line-height: var(--leading-snug); }
.metadata-table td:first-child small,
.metadata-table td:first-child .secondary-name {
    display: block;
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* Technical key and option lists: monospaced, and never allowed to stretch the table.
   Addressed by name rather than by column position - the position rule was written for
   an eight-column table and silently clipped the action buttons once it had six. */
.metadata-table td.metadata-key { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-muted); }
.metadata-table td.metadata-options {
    max-width: 24ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-muted);
}

/* Archived rows recede instead of shouting. */
.data-table tr.inactive td { color: var(--text-subtle); }
.data-table tr.inactive td:first-child { font-weight: var(--weight-regular); }

/* The add-field form sits under the page intro and needs its own breathing room. */
.metadata-form > h2 { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--border-subtle); }
.metadata-form > h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

/* The key field carries a long explanation; keep it under the input, not beside it. */
.metadata-form .field-help { max-width: 46ch; }

/* --------------------------------------------------------------------------
   User administration
   -------------------------------------------------------------------------- */
.user-identity { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.user-identity > div { display: flex; flex-direction: column; min-width: 0; }

/* Both lines truncate rather than wrap. A wrapped address made rows differ in height,
   and long addresses pushed the whole table past its container into a sideways scroll. */
.user-identity strong,
.user-identity small {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.user-identity strong { font-weight: var(--weight-semibold); color: var(--text-strong); line-height: 1.3; }
.user-identity small { font-size: var(--text-xs); color: var(--text-muted); line-height: 1.3; }

/* Auto layout with a floor, not a fixed one.
   table-layout: fixed was here to stop long addresses stretching the table sideways, and
   it did - but it also took "width: 1%" on the actions column literally, sizing it to 13 px
   while its buttons ran out over the column beside them, and under pressure it emptied
   whichever column still had width to give: "Last sign-in" collapsed to zero and its dates
   vanished. The address is now held by the ellipsis on .user-identity, which is what that
   rule was written for, and min-width makes the wrapper scroll rather than squeeze. */
.user-table { table-layout: fixed; }
/* One height for every row, whether the role cell holds a select or a badge. */
.user-table td { vertical-align: middle; height: 56px; }
.user-table th:first-child, .user-table td:first-child { width: auto; }
.user-table th:nth-child(2), .user-table td:nth-child(2) { width: 190px; }
.user-table th:nth-child(3), .user-table td:nth-child(3) { width: 150px; }
.user-table th:nth-child(4), .user-table td:nth-child(4) { width: 160px; }
.user-table select { width: 100%; max-width: 170px; }

/* The action set is the widest thing in the row; give it exactly what it needs. */
.user-table th:last-child, .user-table td:last-child { width: 340px; }

@media (max-width: 1180px) {
    .user-table th:nth-child(4), .user-table td:nth-child(4) { display: none; }
}

/* --------------------------------------------------------------------------
   Profile
   Two columns: who you are and what you may change on the left, what you have
   contributed on the right. Password changes are a rare, deliberate act and
   live in a dialog rather than as a permanent form on the page.
   -------------------------------------------------------------------------- */
.profile-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-5);
    align-items: start;
}
@media (max-width: 1080px) { .profile-layout { grid-template-columns: minmax(0, 1fr); } }

.profile-identity {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding-bottom: var(--space-5);
    margin-bottom: var(--space-5);
    border-bottom: 1px solid var(--border-subtle);
}
.profile-identity > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.profile-identity strong {
    font-size: var(--text-xl);
    font-weight: var(--weight-semibold);
    color: var(--text-strong);
    letter-spacing: -.015em;
    line-height: 1.2;
}
/* Scoped to the text column on purpose. A bare `.profile-identity span` also matched the
   avatar - which is a span - and quietly overrode its colour and size, because an element
   plus class beats a class alone. The same shape of bug greyed out every eyebrow label. */
.profile-identity > div > span { font-size: var(--text-sm); color: var(--text-muted); }
.profile-identity > div > span.profile-since { font-size: var(--text-xs); }

.profile-avatar {
    display: grid; place-items: center;
    width: 68px; height: 68px; flex: 0 0 68px;
    border-radius: var(--radius-pill);
    background: var(--pm-navy-800);
    color: #fff;
    /* Roughly half the diameter: below that the initial floats in the circle. */
    font-size: 2.125rem;
    font-weight: var(--weight-semibold);
    line-height: 1;
    letter-spacing: .01em;
}

/* Facts the administration owns: present, but visibly not editable. */
.profile-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--space-3) var(--space-5);
    margin: var(--space-5) 0 var(--space-3);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-subtle);
}
.profile-facts > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.profile-facts dt {
    font-size: var(--text-xs); font-weight: var(--weight-semibold);
    letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted);
}
.profile-facts dd { margin: 0; font-size: var(--text-sm); color: var(--text-default); overflow-wrap: anywhere; }

.modal.is-narrow { width: min(460px, 100%); }

/* A bar on its own says "some progress"; the number next to it says how much. */
.progress-with-value { display: flex; align-items: center; gap: var(--space-3); }
.progress-with-value .progress-track { flex: 1; min-width: 80px; }
.progress-with-value > span {
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    min-width: 4ch;
    text-align: right;
}
.batch-list-table td:first-child small { display: block; font-size: var(--text-xs); color: var(--text-muted); }
/* Six figures rather than the overview page's four, and inside a table row
   instead of on a page of its own - so only the track width is tightened. The
   tile itself stays exactly the shared .stat-tile, which is the point: the same
   number must not be dressed differently depending on where it is read. */
.batch-list-summary { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--space-3); }

/* Quarantine — the confirmation takes the place of the row's actions, so the
   irreversible click never lands where the harmless one just was. */
.data-table tr.is-pending-delete { background: var(--danger-100); }
.data-table tr.is-pending-delete td { border-color: transparent; }
.destructive-confirm {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-3);
    flex-wrap: wrap;
}
.destructive-confirm > span {
    font-size: var(--text-sm);
    color: var(--danger-600);
    font-weight: var(--weight-medium);
    white-space: normal;
    text-align: right;
}


/* Metadata catalogue -------------------------------------------------------- */
.field-flags { display: flex; gap: var(--space-1); flex-wrap: wrap; margin-top: var(--space-1); }
.flag-pill {
    display: inline-flex;
    align-items: center;
    padding: 1px var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: .01em;
    white-space: nowrap;
}
.flag-pill.is-required { background: var(--pm-orange-100); color: var(--pm-orange-700); }

/* Archived fields stay reachable without competing with the working list. */
.archived-fields > summary {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    cursor: pointer;
    font-weight: var(--weight-semibold);
    color: var(--text-default);
    list-style: none;
}
.archived-fields > summary::-webkit-details-marker { display: none; }
.archived-fields > summary::before {
    content: "";
    width: 8px; height: 8px;
    border-right: 2px solid var(--text-subtle);
    border-bottom: 2px solid var(--text-subtle);
    transform: rotate(-45deg);
    transition: transform var(--duration-fast) var(--ease-out);
}
.archived-fields[open] > summary::before { transform: rotate(45deg); }
.archived-fields > *:not(summary) { margin-top: var(--space-4); }

.checkbox-row { display: flex; gap: var(--space-4); flex-wrap: wrap; align-items: center; }

/* A quiet square control for dismissing a dialog, sized to the header's cap height. */
.icon-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex: none;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.icon-button:hover { background: var(--surface-sunken); color: var(--text-default); }
.icon-button svg { width: 18px; height: 18px; }

/* A short factual line under a page title - status and size at a glance. */
.page-intro-facts {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-top: var(--space-2);
    font-size: var(--text-sm);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* PSF sampling verdict — the one limit that cannot be repaired downstream, so it is
   allowed to be loud when it is bad. */
.flag-pill.is-undersampled, strong.is-undersampled { background: var(--danger-100); color: var(--danger-600); }
.flag-pill.is-marginal, strong.is-marginal { background: var(--warning-100); color: var(--warning-600); }
.flag-pill.is-adequate, strong.is-adequate { background: var(--success-100); color: var(--success-600); }
.psf-summary-grid strong.is-undersampled,
.psf-summary-grid strong.is-marginal,
.psf-summary-grid strong.is-adequate {
    display: inline-flex;
    align-items: center;
    padding: 1px var(--space-2);
    border-radius: var(--radius-pill);
    font-size: var(--text-sm);
}
.notice strong { display: block; margin-bottom: var(--space-1); }
.notice p { margin: 0 0 var(--space-2); }
.notice p:last-child { margin-bottom: 0; }

/* Settings read out of a measurement archive, shown before the import is confirmed. */
.suggestion-list { display: flex; flex-direction: column; gap: 2px; margin: var(--space-2) 0 0; padding: 0; list-style: none; }
.suggestion-list li { display: flex; justify-content: space-between; gap: var(--space-4); font-size: var(--text-sm); }
.suggestion-list li > span { color: var(--text-muted); }
.suggestion-list li > strong { font-variant-numeric: tabular-nums; }

/* The review dialog groups its fields by subject; each group keeps its own heading. */
.review-metadata-form { display: flex; flex-direction: column; gap: var(--space-5); }
.review-metadata-form .metadata-group > h3 {
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--pm-orange-700);
    margin-bottom: var(--space-3);
}

/* Image preview dialog -------------------------------------------------------
   The frame sits centred on its dark ground and is never cropped: judging a
   particle at the edge is exactly what this view is for. */
.image-modal-canvas {
    display: grid;
    place-items: center;
    padding: var(--space-3);
}
.image-modal-canvas img {
    display: block;
    max-width: 100%;
    max-height: min(68vh, 760px);
    object-fit: contain;
}
.image-modal-header > div:first-child { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.image-modal-header strong { font-variant-numeric: tabular-nums; color: var(--text-strong); }
.image-modal-header small { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A display-mode strip sitting under a viewer, using the same tool group as the
   labeling toolbar so the two screens do not invent separate visual languages. */
.viewer-mode-bar { display: flex; justify-content: flex-start; margin-top: var(--space-3); }
.preannotation-settings > label,
.preannotation-side-panel > section > label { display: flex; flex-direction: column; gap: var(--space-1); }

/* Viewer toolbar ------------------------------------------------------------
   One control on every screen that shows a frame. Labeling, preannotation and
   PSF calibration each carried their own copy before, so the same three
   settings looked and read differently depending on where you stood. */
.viewer-toolbar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
}
.viewer-toolbar .spacer { flex: 1; }

/* In the labeling workspace the bar spans the full width of the screen, so it drops the
   rounded box and keeps only the rule that separates it from the stage. */
.workspace-shell > .viewer-toolbar {
    border-width: 0 0 1px;
    border-radius: 0;
    padding-inline: var(--space-5);
}
/* Below the image on the pages that place it there. */
/* The bar sits directly on the stage: square bottom corners, no gap, and the stage picks
   up where it stops. Floating above it with a gap made it read as unrelated furniture
   rather than as the controls for the frame beneath. */
.viewer-toolbar:has(+ .psf-image-stage),
.viewer-toolbar:has(+ .detection-viewer-stage),
.viewer-toolbar:has(+ .image-modal-canvas) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    border-bottom-width: 0;
    margin-bottom: 0;
}
.viewer-toolbar + .psf-image-stage,
.viewer-toolbar + .detection-viewer-stage {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
.psf-canvas-actions { gap: var(--space-3); align-items: center; }
.psf-canvas-actions .viewer-toolbar { flex: 1; }

/* The tool row above a frame: zoom on the left, the shared display controls next to it,
   status text pushed to the end. */
.psf-canvas-actions { justify-content: flex-start; }
.psf-canvas-actions > .psf-fitting-status { margin-left: auto; }

/* PSF sample colours, read the way DynamicPSF reads them: the colour is a state, not a
   decoration. Ordinary samples stay neutral so a hundred of them do not shout; the one
   being worked on is green; a fit that failed or was excluded is red.
   The strokes inherit the hairline widths defined above. */
.psf-vector-fit > * { stroke: rgba(226, 232, 240, .55); }
.psf-vector-fit.focused > * { stroke: #4ecf95; }
.psf-vector-fit.rejected > *,
.psf-vector-fit.failed > * { stroke: #ff7a7a; }

/* The search box is a touch quieter than the fit it encloses, in every state. */
.psf-vector-fit .fit-window { stroke-opacity: .6; }
.psf-vector-fit.focused .fit-window { stroke-opacity: .9; }

/* Metadata read-out ---------------------------------------------------------
   The groups sit side by side and each one stacks its entries; the width is
   spent on showing more groups at once rather than on stretching two of them.

   The earlier arrangement gave every group a full-width row of auto-fit tiles.
   With two entries in a group that produced two columns of 652 px on a wide
   screen - a four-character value with half a metre of empty space after it -
   and the categories, which are the thing worth scanning, ended up scattered
   down the page instead of standing next to each other.

   auto-fill, not auto-fit: auto-fit collapses the empty tracks and stretches
   whatever is left, which is exactly the behaviour being fixed here. */
.metadata-readout {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: var(--space-4) var(--space-5);
    align-items: start;
}
/* The panel's own title introduces every group, so it spans them. */
.metadata-readout > h3 { grid-column: 1 / -1; }

.batch-metadata-group > dl,
.batch-list-metadata-group > dl {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-2);
    margin: 0;
}
.batch-metadata-group > dl > div,
.batch-list-metadata-group > dl > div {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}
.batch-metadata-group dt,
.batch-list-metadata-group dt {
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.batch-metadata-group dd,
.batch-list-metadata-group dd {
    margin: 0;
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
    color: var(--text-strong);
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}
/* The unit belongs to the number without competing with it. */
.batch-metadata-group dd .field-unit,
.batch-list-metadata-group dd .field-unit {
    margin-left: .3em;
    font-size: var(--text-xs);
    font-weight: var(--weight-regular);
    color: var(--text-muted);
}
/* Groups need air between them, not a rule under every heading. */
.batch-metadata { gap: var(--space-5); }
.batch-metadata-group > h3 { border-bottom: 0; padding-bottom: 0; }

/* The reason a preannotation run failed, under its status badge. Kept narrow and wrapping:
   these are exception messages, and one of them ran to several hundred characters. */
.run-failure {
    display: block;
    max-width: 42ch;
    margin-top: var(--space-1);
    color: var(--danger-600);
    font-size: var(--text-xs);
    line-height: var(--leading-snug);
    overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   Viewer settings dialog

   Fields on the left, a live preview on the right. The preview earns its place:
   these are colours for markers that sit on dark, low-contrast microscopy data,
   and a swatch on a white panel says nothing about how they will read there.
   -------------------------------------------------------------------------- */
.viewer-settings-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(200px, 250px);
    gap: var(--space-5);
    align-items: start;
}
@media (max-width: 860px) {
    .viewer-settings-body { grid-template-columns: minmax(0, 1fr); }
}

.viewer-settings-fields { display: flex; flex-direction: column; gap: var(--space-5); min-width: 0; }
.viewer-settings-fields > section { display: flex; flex-direction: column; gap: var(--space-2); }
.viewer-settings-fields h3 {
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin: 0;
}

/* Two columns, not auto-fit. In a wide dialog auto-fit produced three, which left a lone
   field stranded on the next row and put a checkbox between two number fields. */
.viewer-settings-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3) var(--space-4);
    align-items: end;
}
@media (max-width: 560px) { .viewer-settings-grid { grid-template-columns: minmax(0, 1fr); } }
.viewer-settings-grid > .checkbox-label { align-self: center; padding-bottom: 6px; }
.viewer-settings-grid > label { display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--text-sm); }
.viewer-settings-grid .field-label { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.viewer-settings-grid output { font-variant-numeric: tabular-nums; color: var(--text-muted); font-size: var(--text-xs); }
.viewer-settings-grid input[type="range"] { width: 100%; accent-color: var(--pm-orange-500); min-height: 0; padding: 0; border: 0; background: none; }

.viewer-settings-colours {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2) var(--space-4);
}
@media (max-width: 560px) { .viewer-settings-colours { grid-template-columns: minmax(0, 1fr); } }

/* Swatch, name, hex - one row. The picker is how a colour is found, the hex is how one is
   carried in from somewhere else; both edit the same value. */
.colour-field {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) 8.5ch;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    font-size: var(--text-sm);
    padding: 2px 0;
}
.colour-field-name { color: var(--text-default); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The swatch is a block we paint, with the native picker lying invisibly on top of it.
   Left to the browser, the colour was drawn inside the input's own padding, which differs
   per engine; in this control it came out as a thin vertical bar that people read as the
   letter I rather than as the colour it was showing. */
.colour-field-swatch {
    position: relative;
    display: block;
    width: 38px;
    height: 26px;
    border: 1px solid var(--border-default);
    border-radius: var(--radius-sm);
    cursor: pointer;
    overflow: hidden;
}
.colour-field-swatch input[type="color"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    opacity: 0;
    cursor: pointer;
}
.colour-field-hex {
    min-width: 0;
    min-height: 26px;
    padding: 2px var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    text-transform: lowercase;
    text-align: center;
}

/* The one deliberately dark surface in the dialog - the ground the markers actually
   sit on. Fixed rather than themed, because that is what the editor shows. */
.viewer-preview-stage {
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: #12181d;
    padding: var(--space-2);
}
.viewer-preview-stage svg { display: block; width: 100%; height: auto; }

.viewer-preview-legend {
    list-style: none;
    margin: var(--space-3) 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    font-size: var(--text-xs);
    color: var(--text-muted);
}
.viewer-preview-legend li { display: flex; align-items: center; gap: var(--space-2); }
.legend-dot { width: 10px; height: 10px; border-radius: 2px; flex: 0 0 10px; }

.viewer-settings-preview { position: sticky; top: 0; }
.viewer-settings-preview h3 {
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin: 0 0 var(--space-1);
}
.field-help.is-error { color: var(--danger-600); }

/* --------------------------------------------------------------------------
   Particle measurement dialog

   The measured region on the left, what it means on the right. The region is
   shown rather than described because the labeler's own eyes are the point of
   reference this is meant to inform - a column of numbers alone would be asking
   them to trust it instead of check it.
   -------------------------------------------------------------------------- */
.measurement-body {
    display: grid;
    grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
    gap: var(--space-5);
    align-items: start;
}
@media (max-width: 820px) { .measurement-body { grid-template-columns: minmax(0, 1fr); } }

.measurement-view { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.measurement-stage {
    position: relative;
    aspect-ratio: 1;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: #0d1216;
    overflow: hidden;
}
/* Hard pixel edges: this is a 25 px patch blown up, and smoothing it would invent
   detail exactly where somebody is trying to judge whether there is any. */
.measurement-stage img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    image-rendering: pixelated;
}
.measurement-stage svg { position: absolute; inset: 0; width: 100%; height: 100%; }
/* non-scaling-stroke earns its keep here, unlike on the PSF page. There the magnification
   came from a CSS transform on an ancestor, which it cannot see; here it is the SVG's own
   viewBox, which is exactly what it compensates for. Without it a stroke of one patch pixel
   would be drawn about thirteen screen pixels wide and bury the particle. */
.measurement-ellipse {
    fill: none;
    stroke: var(--marker-fit, #7ee0e8);
    stroke-width: 1;
    /* Finer than the markers on the frame: at the FWHM this corpus produces the ellipse is
       barely two pixels across, and a 3 px dash would leave it as two specks. */
    stroke-dasharray: 2 1.5;
    vector-effect: non-scaling-stroke;
}
.measurement-centre line {
    stroke: var(--marker-psf-centre, var(--marker-selected));
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
}

.measurement-readout { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }

.measurement-verdict {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--space-3) var(--space-4);
    border-left: 3px solid currentColor;
    border-radius: var(--radius-sm);
    background: var(--surface-sunken);
}
.measurement-verdict strong { font-size: var(--text-base); font-weight: var(--weight-semibold); }
.measurement-verdict span { font-size: var(--text-xs); color: var(--text-muted); line-height: var(--leading-snug); }
.measurement-verdict.is-strong { color: var(--success-600); }
.measurement-verdict.is-good { color: var(--success-600); }
.measurement-verdict.is-uncertain { color: var(--warning-600); }
.measurement-verdict.is-weak { color: var(--danger-600); }

.measurement-badges { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.measurement-values {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-1) var(--space-4);
    margin: 0;
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
}
.measurement-values dt { color: var(--text-muted); white-space: nowrap; }
.measurement-values dd { margin: 0; color: var(--text-strong); }
.measurement-values dd small { margin-left: var(--space-2); color: var(--text-muted); font-variant-numeric: normal; }

.measurement-notes {
    list-style: none;
    margin: 0;
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--surface-sunken);
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    font-size: var(--text-xs);
    line-height: var(--leading-snug);
    color: var(--text-muted);
}

.measurement-models summary { cursor: pointer; font-size: var(--text-sm); color: var(--text-link); }
.measurement-models .data-table { font-size: var(--text-xs); }
.measurement-models tr.is-selected { background: var(--pm-orange-100); }
.measurement-models tr.is-selected td { color: var(--text-strong); font-weight: var(--weight-medium); }

/* --------------------------------------------------------------------------
   Frame scan dialog
   -------------------------------------------------------------------------- */
.frame-scan-progress { display: flex; flex-direction: column; gap: var(--space-2); }

.frame-scan-tally {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: var(--space-2);
    margin: 0;
}
.frame-scan-tally > div {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--space-3);
    border-left: 3px solid currentColor;
    border-radius: var(--radius-sm);
    background: var(--surface-sunken);
}
.frame-scan-tally dt { font-size: var(--text-xs); color: var(--text-muted); }
.frame-scan-tally dd {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}
.frame-scan-tally .is-strong { color: var(--success-600); }
.frame-scan-tally .is-good { color: var(--success-600); }
.frame-scan-tally .is-uncertain { color: var(--warning-600); }
.frame-scan-tally .is-weak { color: var(--danger-600); }

/* --------------------------------------------------------------------------
   Background jobs
   -------------------------------------------------------------------------- */
.job-list { display: flex; flex-direction: column; gap: var(--space-3); }

.job-row {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
}
.job-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-2) var(--space-3); }
.job-head strong { color: var(--text-strong); }
.job-meta { font-size: var(--text-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }

.job-progress { display: flex; align-items: center; gap: var(--space-3); }
.job-progress .progress-track { flex: 1 1 auto; }
/* Wide enough for "1234 / 5678" without the bar resizing as the numbers grow. */
.job-progress > span {
    font-size: var(--text-xs); color: var(--text-muted);
    font-variant-numeric: tabular-nums; min-width: 11ch; text-align: right;
}

/* Work whose extent is unknown. It moves so the row does not read as stalled, but it
   never claims a position it does not have. */
.progress-track.is-indeterminate { overflow: hidden; }
.progress-track.is-indeterminate .progress-fill {
    width: 35%;
    animation: job-sweep 1.4s ease-in-out infinite;
}
@keyframes job-sweep {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(300%); }
}
@media (prefers-reduced-motion: reduce) {
    .progress-track.is-indeterminate .progress-fill { animation: none; width: 100%; opacity: .4; }
}

.job-foot { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.job-foot .spacer { flex: 1 1 var(--space-4); }
.job-state { font-size: var(--text-xs); color: var(--text-muted); }

/* The jobs entry is a button but has to sit in a column of links without looking different. */
.sidebar-jobs { width: 100%; border: 0; background: none; font: inherit; text-align: left; cursor: pointer; }


/* Export ---------------------------------------------------------------------
   The datasets are a list that grows; the two constraints beside them are a fixed
   pair. Sharing one auto-fit grid put a single dataset alone in a third of the
   width and wrapped the two fields underneath it, which read as three unrelated
   fragments rather than one filter. */
.export-filter { display: flex; flex-direction: column; gap: var(--space-5); }
.export-filter-datasets { display: flex; flex-direction: column; gap: var(--space-2); }
.export-filter-limits {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--space-4) var(--space-5);
    align-items: start;
}
.export-filter-limits > label { display: flex; flex-direction: column; gap: var(--space-1); }

/* auto-fill, not auto-fit: with one dataset the checkbox should stay its own width
   rather than stretch across the panel. */
.checkbox-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--space-2) var(--space-4);
}
.checkbox-grid .checkbox-label { display: flex; align-items: center; gap: var(--space-2); }
/* The count belongs to the name, on the same line, and stops competing with it. */
.checkbox-grid .checkbox-label .field-help { margin-left: .4em; }

/* The coverage bars sit under the tiles and repeat their vocabulary; they need less
   air than a list of their own would. */
.export-coverage { margin-top: var(--space-5); gap: var(--space-1); }
.export-coverage .distribution-label { font-weight: var(--weight-medium); color: var(--text-strong); }

/* The switch sits under the pair of constraints and applies to both, so it gets its own
   line rather than a third column. */
.export-filter-new-only { align-items: flex-start; gap: var(--space-2); }
.export-filter-new-only .field-help { display: block; margin-top: 2px; }

/* Export facets --------------------------------------------------------------
   Values as chips rather than a table: the list is short per field, the count
   belongs to the value it qualifies, and choosing is one click on the thing
   itself instead of a checkbox beside a label. */
/* Side by side, because a facet is a label and a handful of short chips - stacked, five
   of them filled a screen while two thirds of every row stayed empty. Wide enough that a
   field with several values still keeps its chips on one or two lines. */
.export-facets {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: var(--space-4) var(--space-5);
    align-items: start;
}
.export-facet { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
/* The line about what the whole corpus agrees on introduces nothing and follows everything,
   so it spans the columns rather than sitting in one of them. */
.export-facets-uniform { grid-column: 1 / -1; }
.facet-values { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.facet-chip {
    display: inline-flex; align-items: baseline; gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    background: var(--surface-raised);
    font: inherit; font-size: var(--text-sm);
    color: var(--text-default);
    cursor: pointer;
}
.facet-chip:hover:not(:disabled) { border-color: var(--border-strong); background: var(--surface-hover); }
.facet-chip.is-selected {
    border-color: transparent;
    background: var(--pm-navy-700);
    color: #fff;
}
.facet-chip.is-selected .facet-chip-count,
.facet-chip.is-selected .field-unit { color: rgba(255, 255, 255, .75); }

/* Kept visible rather than removed: a combination that yields nothing is worth
   seeing, and a value that simply vanished reads as one that never existed. */
.facet-chip.is-empty { opacity: .45; cursor: not-allowed; }

.facet-chip-count { font-size: var(--text-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.export-facets-uniform { margin: 0; }

/* Reads as a control, not as a value: no border, no pill, so it does not look like one
   more thing to choose. */
.facet-more {
    border: 0; background: none; padding: var(--space-1) var(--space-2);
    font: inherit; font-size: var(--text-sm);
    color: var(--pm-navy-600); text-decoration: underline; text-underline-offset: 2px;
    cursor: pointer;
}
.facet-more:hover { color: var(--pm-navy-700); }

/* "not recorded" is an absence, so it is set apart from the values beside it. */
.facet-chip-label em { font-style: italic; color: var(--text-muted); }
.facet-chip.is-selected .facet-chip-label em { color: rgba(255, 255, 255, .85); }

/* The category mark sits with the label, not in front of it: same colour, same weight,
   so it reads as part of the heading rather than as decoration beside it. */
.facet-label,
.metadata-group-heading { display: inline-flex; align-items: center; gap: var(--space-2); }
.metadata-group-icon {
    width: 1em; height: 1em; flex: 0 0 auto;
    stroke: currentColor; fill: none;
    stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
    opacity: .8;
}
/* Sized off the heading it belongs to, so it grows and shrinks with the text instead of
   being pinned to one pixel size that only suits one of the three places it appears. */
.metadata-group-heading .metadata-group-icon { width: 1.05em; height: 1.05em; }

/* Facet groups ---------------------------------------------------------------
   The category is a column heading with a rule under it; its fields hang below.
   Named once per group rather than once per field - the same camera in front of
   Gain, Kamera and Shutter said three times what the heading says once. */
.facet-group { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.facet-group-heading {
    display: flex; align-items: center; gap: var(--space-2);
    margin: 0; padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border-subtle);
    font-size: var(--text-xs); font-weight: var(--weight-semibold);
    letter-spacing: .07em; text-transform: uppercase; color: var(--pm-orange-700);
}
/* The all-datasets box leads the list and is set slightly apart from the names under it. */
.checkbox-grid .checkbox-label.is-all { grid-column: 1 / -1; font-weight: var(--weight-medium); }

/* Dataset picker -------------------------------------------------------------
   Bounded and scrollable, because the catalogue grows and this list must not push
   the rest of the filter off the screen. */
.dataset-head { display: flex; align-items: baseline; gap: var(--space-3); }
.dataset-search { max-width: 320px; }
.dataset-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--space-2) var(--space-4);
    max-height: 190px;
    overflow-y: auto;
    padding: var(--space-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
}
/* Switched off rather than hidden while "all" holds: a list that disappears leaves
   the reader wondering what they would have been choosing from. */
.dataset-list.is-disabled { opacity: .5; }
.dataset-list.is-disabled .checkbox-label { cursor: not-allowed; }

/* An eyebrow that carries a mark: the icon sits on the baseline of the small caps
   rather than towering over them. */
.eyebrow-with-icon { display: inline-flex; align-items: center; gap: var(--space-2); }
.eyebrow-with-icon .metadata-group-icon { width: 1.15em; height: 1.15em; }

/* Erkannte Werte, die in kein Feld gehen. Zurueckgenommen dargestellt: sie sind eine Auskunft,
   keine Handlungsaufforderung - aber sie sollen auffindbar sein, statt zu verschwinden. */
.suggestion-list.is-muted { opacity: .72; }
.suggestion-list.is-muted strong { font-weight: var(--weight-regular); }
