/* ==========================================================================
   ParticleLabeler - application styles
   Depends on tokens.css. Organised as: reset, base, shell, components.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Reset and base
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    min-height: 100vh;
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    color: var(--text-default);
    background: var(--surface-canvas);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { margin: 0; line-height: var(--leading-tight); color: var(--text-strong); font-weight: var(--weight-semibold); }
h1 { font-size: var(--text-2xl); letter-spacing: -0.02em; }
h2 { font-size: var(--text-lg); letter-spacing: -0.01em; }
h3 { font-size: var(--text-md); }
h4 { font-size: var(--text-base); }
p { margin: 0; }

a { color: var(--text-link); text-decoration: none; }
a:hover { color: var(--text-link-hover); text-decoration: underline; }

img, svg, canvas { display: block; max-width: 100%; }

/* Every inline icon in this application is a stroked outline. Without a default the
   browser fills the path solid black, which turns an eye into a dark block - and it
   only shows up wherever a component forgot to set it. */
button svg, a svg, label svg, .icon svg {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; border-radius: var(--radius-sm); }

/* Chrome must not be text-selectable: dragging a particle would otherwise
   start a text selection. Inputs and .selectable regions opt back in. */
.app-shell, .workspace-shell { -webkit-user-select: none; user-select: none; }
input, textarea, select, [contenteditable="true"], .selectable, code, pre { -webkit-user-select: text; user-select: text; }

.visually-hidden {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
}

/* --------------------------------------------------------------------------
   2. Application shell

   A white brand bar across the top, a navy navigation column beneath it on the
   left, light content beside it. The logo always sits on white: it is a
   fixed-colour raster and simply disappears on a dark ground.
   -------------------------------------------------------------------------- */
.app-shell {
    display: grid;
    grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
    grid-template-rows: var(--brandbar-height) minmax(0, 1fr);
    grid-template-areas:
        "brand brand"
        "nav   main";
    min-height: 100vh;
}
.app-shell.is-collapsed { grid-template-columns: var(--sidebar-width-collapsed) minmax(0, 1fr); }

/* --- Brand bar --- */
.app-brandbar {
    grid-area: brand;
    position: sticky;
    top: 0;
    z-index: calc(var(--z-sticky) + 1);
    display: flex;
    align-items: center;
    gap: 0;
    padding: 0 var(--space-5) 0 0;
    background: var(--neutral-0);
    border-bottom: 1px solid var(--border-subtle);
}

/* The logo block is exactly as wide as the navigation column, so the divider that
   follows it lands on the same vertical line as the column edge below. */
.brandbar-logo {
    display: flex;
    align-items: center;
    flex: 0 0 var(--sidebar-width);
    width: var(--sidebar-width);
    padding: 0 var(--space-4) 0 var(--space-5);
}
.brandbar-logo img { width: 100%; max-width: 186px; height: 54px; object-fit: contain; object-position: left center; }

.brandbar-title {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-width: 0;
    padding-left: var(--space-5);
    border-left: 1px solid var(--border-subtle);
}

/* Collapsed, the column is far too narrow for a wordmark. The logo keeps its size
   and the divider steps aside rather than cutting the logo in half. */
.app-shell.is-collapsed .brandbar-logo { flex: 0 0 auto; width: auto; }
.app-shell.is-collapsed .brandbar-title { border-left: 0; padding-left: var(--space-4); }
.brandbar-title strong { font-size: var(--text-md); font-weight: var(--weight-semibold); color: var(--text-strong); letter-spacing: -.01em; }
.brandbar-title span { font-size: var(--text-sm); color: var(--text-muted); }

.brandbar-actions { display: flex; align-items: center; gap: var(--space-3); margin-left: auto; }
.brandbar-user {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-md);
    color: inherit;
}
.brandbar-user:hover { background: var(--surface-hover); text-decoration: none; }

.user-avatar {
    display: grid; place-items: center;
    width: 32px; height: 32px; flex: 0 0 32px;
    border-radius: var(--radius-pill);
    background: var(--pm-navy-800); color: #fff;
    font-size: var(--text-md); font-weight: var(--weight-semibold);
    line-height: 1;
}
.user-name { font-size: var(--text-sm); color: var(--text-default); font-weight: var(--weight-medium); }

/* --- Navigation column --- */
.app-sidebar {
    grid-area: nav;
    position: sticky;
    top: var(--brandbar-height);
    display: flex;
    flex-direction: column;
    height: calc(100vh - var(--brandbar-height));
    padding: var(--space-4) var(--space-3);
    background: var(--pm-navy-800);
    overflow: hidden auto;
    scrollbar-width: thin;
    scrollbar-color: var(--pm-navy-600) transparent;
}

.sidebar-group { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--space-4); }
.sidebar-group-label {
    padding: var(--space-2) var(--space-2) var(--space-1);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: .09em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, .38);
}

.sidebar-link {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md);
    color: rgba(255, 255, 255, .80);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    white-space: nowrap;
    transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.sidebar-link:hover { background: rgba(255, 255, 255, .08); color: #fff; text-decoration: none; }

/* The active entry is marked in the corporate orange, which is exactly how the
   website uses it: to draw the eye, never as a button colour. */
.sidebar-link.active {
    background: rgba(255, 87, 51, .14);
    color: #fff;
    box-shadow: inset 3px 0 0 var(--pm-orange-500);
}
.sidebar-link svg { width: 18px; height: 18px; flex: 0 0 18px; stroke: currentColor; fill: none; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.sidebar-link.active svg { stroke: var(--pm-orange-400); }
.sidebar-link-text { overflow: hidden; text-overflow: ellipsis; }

.sidebar-badge {
    margin-left: auto;
    padding: 1px var(--space-2);
    border-radius: var(--radius-pill);
    background: rgba(255, 255, 255, .16);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
}
.sidebar-link.active .sidebar-badge { background: var(--pm-orange-500); color: #fff; }
.sidebar-badge.is-busy { background: var(--pm-orange-500); color: #fff; }

.sidebar-footer { margin-top: auto; padding-top: var(--space-4); border-top: 1px solid rgba(255, 255, 255, .12); }
.sidebar-collapse {
    display: flex; align-items: center; justify-content: center; gap: var(--space-2);
    width: 100%; padding: var(--space-2);
    border: 0; border-radius: var(--radius-md);
    background: transparent; color: rgba(255, 255, 255, .6);
    font: inherit; font-size: var(--text-sm); cursor: pointer;
}
.sidebar-collapse:hover { background: rgba(255, 255, 255, .08); color: #fff; }
.sidebar-collapse svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

.app-shell.is-collapsed .sidebar-group-label { visibility: hidden; height: var(--space-4); padding: 0; }
.app-shell.is-collapsed .sidebar-link { justify-content: center; padding: var(--space-3) 0; }
.app-shell.is-collapsed .sidebar-link-text,
.app-shell.is-collapsed .sidebar-badge,
.app-shell.is-collapsed .sidebar-collapse span { display: none; }

/* --- Content --- */
.app-main {
    grid-area: main;
    width: 100%;
    max-width: var(--content-max);
    margin: 0 auto;
    padding: var(--space-6);
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    min-width: 0;
}
.app-main.is-flush { max-width: none; padding: 0; gap: 0; }

@media (max-width: 1100px) {
    .app-shell { grid-template-columns: var(--sidebar-width-collapsed) minmax(0, 1fr); }
    .app-sidebar .sidebar-link { justify-content: center; padding: var(--space-3) 0; }
    .app-sidebar .sidebar-link-text,
    .app-sidebar .sidebar-badge,
    .app-sidebar .sidebar-group-label,
    .app-sidebar .sidebar-collapse { display: none; }
    .app-main { padding: var(--space-4); }
    .app-brandbar { padding: 0 var(--space-4) 0 0; }
    .brandbar-logo { flex: 0 0 auto; width: auto; padding-left: var(--space-4); }
    .brandbar-title { display: none; }
}

/* --------------------------------------------------------------------------
   3. Page primitives
   -------------------------------------------------------------------------- */
.page-intro { display: flex; align-items: flex-start; gap: var(--space-4); flex-wrap: wrap; }
.page-intro > div:first-child { flex: 1; min-width: 260px; display: flex; flex-direction: column; gap: var(--space-1); }
.page-intro p:not(.eyebrow) { color: var(--text-muted); max-width: 76ch; }

.eyebrow {
    margin: 0;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--pm-orange-700);
}

.panel {
    padding: var(--space-5);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    box-shadow: var(--shadow-xs);
}
.panel.is-flush { padding: 0; overflow: hidden; }
.panel.is-quiet { background: var(--surface-sunken); box-shadow: none; }

.section-heading {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
}
.section-heading > div:first-child { display: flex; flex-direction: column; gap: var(--space-1); min-width: 240px; flex: 1; }
/* A section description sits in a flex column that is already bounded by the panel and by
   the count beside it. A second cap on top of that broke short sentences onto a stray last
   line while three quarters of the row stayed empty. */
.section-heading p:not(.eyebrow) { color: var(--text-muted); font-size: var(--text-sm); }
.section-count { font-size: var(--text-sm); color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* The count and whatever acts on the section, on one line at its right edge. Not the
   first child, so the heading's flex:1 stays with the text where it belongs. */
.section-heading-actions { display: flex; align-items: center; gap: var(--space-3); }

.panel-intro, .field-help, .form-hint, .phr-sub { font-size: var(--text-sm); color: var(--text-muted); }

.grid { display: grid; gap: var(--space-4); }
.grid.cols-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.grid.cols-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.grid.cols-4 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

.stack { display: flex; flex-direction: column; gap: var(--space-4); }
.row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.row.is-end { justify-content: flex-end; }
.spacer { flex: 1; }

/* --------------------------------------------------------------------------
   4. Buttons
   -------------------------------------------------------------------------- */
.btn, .primary-button, .table-action-button, .create-batch-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);
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--duration-fast) var(--ease-out),
                border-color var(--duration-fast) var(--ease-out),
                box-shadow var(--duration-fast) var(--ease-out),
                transform var(--duration-fast) var(--ease-out);
}
.btn:hover:not(:disabled), .table-action-button:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--border-strong); }
.btn:active:not(:disabled), .table-action-button:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled, .primary-button:disabled, .table-action-button:disabled { opacity: .5; cursor: not-allowed; }

.btn svg, .primary-button svg, .table-action-button svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

.primary-button, .create-batch-button, .btn.is-primary {
    border-color: transparent;
    background: var(--pm-navy-800);
    color: var(--text-on-brand);
    box-shadow: var(--shadow-xs);
}
.primary-button:hover:not(:disabled), .create-batch-button:hover:not(:disabled), .btn.is-primary:hover:not(:disabled) {
    background: var(--pm-navy-700);
    box-shadow: var(--shadow-sm);
}

/* The website has no filled orange button: its accent buttons are white with an
   orange label and an orange outline. Copying that is both truer to the brand and
   readable - white on orange only reaches 4:1, short of the 4.5:1 needed for text. */
.btn.is-accent {
    background: var(--neutral-0);
    border-color: var(--pm-orange-500);
    color: var(--pm-orange-700);
}
.btn.is-accent:hover:not(:disabled) {
    background: var(--pm-orange-100);
    border-color: var(--pm-orange-600);
    color: var(--pm-orange-700);
}

.btn.destructive, .table-action-button.destructive, .btn.is-danger {
    border-color: var(--danger-500);
    color: var(--danger-600);
    background: var(--surface-raised);
}
.btn.destructive:hover:not(:disabled), .table-action-button.destructive:hover:not(:disabled) { background: var(--danger-100); }

.btn.is-small, .table-action-button.is-small { min-height: 28px; padding: var(--space-1) var(--space-3); font-size: var(--text-xs); }
.btn.is-large { min-height: 42px; padding: var(--space-3) var(--space-5); font-size: var(--text-base); }
.btn.active, .table-action-button.active { background: var(--pm-navy-700); border-color: transparent; color: #fff; }

.link-button, .text-button {
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-link);
    font: inherit;
    font-size: var(--text-sm);
    cursor: pointer;
    text-decoration: none;
}
.link-button:hover, .text-button:hover { text-decoration: underline; }

.icon-tool-button {
    display: grid; place-items: center;
    width: 30px; height: 30px;
    border: 1px solid var(--border-default);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
    color: var(--text-default);
    cursor: pointer;
    transition: background var(--duration-fast) var(--ease-out);
}
.icon-tool-button:hover { background: var(--surface-hover); }
.icon-tool-button svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

.table-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.form-buttons { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-4); }

/* --------------------------------------------------------------------------
   5. Forms
   -------------------------------------------------------------------------- */
/* Labels in this codebase wrap their own control, and the caption is often a bare text
   node rather than a span. A column flexbox puts the caption above the control in both
   cases, without needing every page to be rewritten. */
label {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    align-items: stretch;
}
label > input, label > select, label > textarea { width: 100%; }

.field-label, label > span:first-child {
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--text-default);
}

/* Wrapper around a control plus its unit. Not a control itself - styling it like one
   produced a box inside a box and pushed every field past the dialog edge. */
.field-control {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}
.field-control > input, .field-control > select { flex: 1; min-width: 0; }

input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="submit"]):not([type="button"]),
textarea, select {
    width: 100%;
    padding: var(--space-2) var(--space-3);
    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);
    transition: border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
}
/* A native select renders a system-drawn arrow and its own metrics, so next to a text
   input it sits at a different height with a different border radius. Drawing the chevron
   ourselves makes both controls one family. */
select {
    appearance: none;
    padding-right: calc(var(--space-3) + 18px);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7688' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--space-3) center;
    background-size: 16px 16px;
    cursor: pointer;
}
.theme-workspace select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2393a1b3' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

/* One height for every control, so a row of mixed inputs and selects lines up. */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="submit"]):not([type="button"]),
select { height: 36px; min-height: 36px; }

input:hover:not(:disabled), textarea:hover:not(:disabled), select:hover:not(:disabled) { border-color: var(--border-strong); }
input:focus, textarea:focus, select:focus {
    outline: none;
    border-color: var(--border-focus);
    box-shadow: var(--shadow-focus);
}
input:disabled, textarea:disabled, select:disabled { background: var(--surface-sunken); color: var(--text-subtle); cursor: not-allowed; }
textarea { min-height: 84px; resize: vertical; line-height: var(--leading-normal); }

input[type="checkbox"], input[type="radio"] {
    width: 16px; height: 16px; min-height: 0;
    accent-color: var(--pm-navy-600);
    cursor: pointer;
}

.checkbox-label, .check-label {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    font-weight: var(--weight-regular);
    cursor: pointer;
    margin-bottom: 0;
}
.checkbox-label input, .check-label input { flex: 0 0 auto; width: 16px; }

.field-help { display: block; margin-top: var(--space-1); font-size: var(--text-xs); line-height: var(--leading-snug); }
.field-unit, .inline-field-unit { font-size: var(--text-xs); color: var(--text-muted); }
.required-mark { color: var(--danger-500); margin-left: 2px; }

/* Field grids
   Captions differ in length and wrap at different points, which used to leave every
   control in a row at its own height. Each label spans three shared rows - caption,
   control, hint - so the three line up across the whole row. */
.form-grid, .field-grid {
    display: grid;
    gap: var(--space-2) var(--space-4);
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    align-items: start;
}
.form-grid > label, .field-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) {
    .form-grid > label, .field-grid > label {
        grid-row: span 3;
        grid-template-rows: subgrid;
    }
}
/* A checkbox is a single row and must not be stretched across the three field rows. */
.form-grid > .checkbox-label, .field-grid > .checkbox-label {
    grid-row: auto;
    display: flex;
    align-self: center;
}
.form-grid .wide-field, .field-grid .wide-field { grid-column: 1 / -1; }

/* Zwei Bedienelemente, die als eines zaehlen sollen: ein Auswahlfeld und das Textfeld, das es
   bei Bedarf aufklappt. Ohne diese Klammer waeren es zwei Kinder des label, das Raster hat aber
   nur drei Zeilen - das vierte Kind legt sich dann ueber den Hinweistext darunter. */
.stacked-control { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }

fieldset { margin: 0; padding: 0; border: 0; }

/* --------------------------------------------------------------------------
   6. Tables
   -------------------------------------------------------------------------- */
.table-wrap {
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    overflow: auto hidden;
}
.panel > .table-wrap { border-radius: var(--radius-md); }

table, .data-table, .batch-list-table, .import-table, .metadata-table, .psf-fit-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
}

thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: var(--space-3) var(--space-4);
    background: var(--surface-sunken);
    border-bottom: 1px solid var(--border-subtle);
    text-align: left;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
}

tbody td { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border-subtle); vertical-align: middle; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr { transition: background var(--duration-fast) var(--ease-out); }
tbody tr:hover { background: var(--surface-hover); }
tbody tr.is-selected, tbody tr.selected { background: var(--info-100); }

.numeric, .is-numeric { text-align: right; font-variant-numeric: tabular-nums; }
.mono { font-family: var(--font-mono); font-size: var(--text-xs); }

.pagination-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding-top: var(--space-4);
    font-size: var(--text-sm);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   7. Status, badges, notices
   -------------------------------------------------------------------------- */
.status-badge, .status-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 2px 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-semibold);
    white-space: nowrap;
}
.status-badge::before, .status-pill::before {
    content: "";
    width: 6px; height: 6px;
    border-radius: 50%;
    background: currentColor;
}
.status-badge.is-success, .status-completed, .status-badge.status-completed { background: var(--success-100); color: var(--success-600); }
.status-badge.is-warning, .status-processing, .status-badge.status-processing { background: var(--warning-100); color: var(--warning-600); }
.status-badge.is-danger, .status-failed, .status-badge.status-failed, .rejected { background: var(--danger-100); color: var(--danger-600); }
.status-badge.is-info, .status-queued, .status-badge.status-queued { background: var(--info-100); color: var(--info-600); }
.status-badge.is-neutral, .inactive { background: var(--surface-sunken); color: var(--text-muted); }

.notice {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border-subtle);
    border-left: 3px solid var(--text-muted);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
    font-size: var(--text-sm);
    box-shadow: var(--shadow-xs);
}
.notice-success { border-left-color: var(--success-500); background: var(--success-100); color: var(--success-600); }
.notice-error, .auth-error { border-left-color: var(--danger-500); background: var(--danger-100); color: var(--danger-600); }
.notice-warning { border-left-color: var(--warning-500); background: var(--warning-100); color: var(--warning-600); }
.notice-info { border-left-color: var(--info-500); background: var(--info-100); color: var(--info-600); }

.presence-online, .psf-ready { color: var(--success-600); font-weight: var(--weight-medium); }
.presence-offline, .psf-not-ready { color: var(--text-muted); }
.online-since { font-size: var(--text-xs); color: var(--text-muted); }

/* --- Progress --- */
.progress-track, .job-progress-track, .upload-progress {
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    overflow: hidden;
}
/* .job-progress used to be listed here as a second fill class. It is now the name of the
   row that WRAPS a bar in the jobs dialog, so this rule painted the whole row navy and
   put the label on top of it - which looked like a bar stuck at nearly full while the
   real one, six pixels tall, sat inside showing its actual four percent. */
.progress-fill {
    height: 100%;
    border-radius: var(--radius-pill);
    background: var(--pm-navy-600);
    transition: width var(--duration-slow) var(--ease-out);
}
.progress-fill.is-success { background: var(--success-500); }
.progress-fill.is-warning { background: var(--warning-500); }

/* --- Stats --- */
.stat-grid { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
.stat-tile {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-4);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
}
.stat-tile .stat-label { font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
.stat-tile .stat-value { font-size: var(--text-2xl); font-weight: var(--weight-semibold); color: var(--text-strong); font-variant-numeric: tabular-nums; line-height: 1.1; }
.stat-tile .stat-hint { font-size: var(--text-xs); color: var(--text-muted); }
.stat-tile.is-accent { border-color: var(--pm-orange-400); background: var(--pm-orange-100); }

/* --- Distribution bars (particle type coverage) --- */
.distribution-list { display: flex; flex-direction: column; gap: var(--space-2); }
.distribution-row { display: grid; grid-template-columns: minmax(120px, 1.2fr) minmax(0, 3fr) auto; align-items: center; gap: var(--space-3); font-size: var(--text-sm); }
.distribution-row .distribution-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.distribution-row .distribution-bar { height: 8px; border-radius: var(--radius-pill); background: var(--surface-sunken); overflow: hidden; }
.distribution-row .distribution-bar span { display: block; height: 100%; border-radius: var(--radius-pill); background: var(--pm-navy-600); }
.distribution-row .distribution-value { font-variant-numeric: tabular-nums; color: var(--text-muted); min-width: 4ch; text-align: right; }
.distribution-row.is-low .distribution-bar span { background: var(--warning-500); }
.distribution-row.is-low .distribution-label { color: var(--warning-600); font-weight: var(--weight-medium); }

/* --- Empty state --- */
.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-7) var(--space-5);
    text-align: center;
    color: var(--text-muted);
}
.empty-state svg { width: 40px; height: 40px; stroke: var(--text-subtle); fill: none; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.empty-state strong { color: var(--text-default); font-size: var(--text-md); font-weight: var(--weight-semibold); }
.empty-state p { max-width: 46ch; font-size: var(--text-sm); }

/* --------------------------------------------------------------------------
   8. Overlays: modal, backdrop
   -------------------------------------------------------------------------- */
.modal-backdrop, .batch-create-backdrop, .psf-cancel-backdrop, .image-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    display: grid;
    place-items: center;
    padding: var(--space-5);
    background: rgba(10, 16, 24, .5);
    backdrop-filter: blur(3px);
}
.modal, .batch-create-modal, .psf-cancel-dialog, .image-modal {
    position: relative;
    z-index: var(--z-modal);
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    width: min(680px, 100%);
    max-height: min(86vh, 900px);
    padding: var(--space-5);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-xl);
    background: var(--surface-raised);
    box-shadow: var(--shadow-lg);
    /* The dialog itself does not scroll; its body does. Scrolling the whole thing took the
       title and the decisions out of view together with the content, so a long dialog lost
       both what it was about and how to leave it. */
    overflow: hidden;
}
/* The part between header and footer is what gives way when the content is taller than the
   screen. Everything that says what this is, and everything that ends it, stays put. */
.modal > .modal-header,
.modal > .modal-footer { flex: 0 0 auto; }
.modal > :not(.modal-header):not(.modal-footer) { overflow: auto; min-height: 0; }
.modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); }
.modal-footer {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    /* A narrow dialog cannot always fit a secondary action and two decisions on one
       line; wrapping is better than a sideways scrollbar inside a dialog. */
    flex-wrap: wrap;
    justify-content: flex-end;
    padding-top: var(--space-4);
    margin-top: var(--space-2);
    border-top: 1px solid var(--border-subtle);
}
.modal-footer > .spacer { flex: 1 1 var(--space-4); }
.modal.is-wide { width: min(1080px, 100%); }

/* --------------------------------------------------------------------------
   9. Authentication screens
   -------------------------------------------------------------------------- */
/* The sign-in screen is the one place the brand gets the whole surface. A clean
   navy field: an orange wash over near-black navy turns muddy brown, so the accent
   appears once, as a hairline on the card, rather than as atmosphere. */
.auth-shell {
    display: grid;
    place-items: center;
    min-height: 100vh;
    padding: var(--space-5);
    background:
        radial-gradient(ellipse 120% 80% at 50% 120%, rgba(26, 80, 145, .55), transparent 65%),
        linear-gradient(180deg, var(--pm-navy-900) 0%, var(--pm-navy-800) 100%);
}
.auth-card {
    position: relative;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-6);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    box-shadow: 0 24px 60px rgba(0, 0, 0, .35);
    overflow: hidden;
}
.auth-card::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: var(--pm-orange-500);
}
.auth-main { display: flex; flex-direction: column; align-items: center; gap: var(--space-4); width: min(430px, 100%); }
/* The logo is a fixed-colour raster and would vanish on the brand gradient, so it sits
   on the same light plate used in the sidebar rather than being inverted. */
/* The logo lives inside the card now, so it needs no plate of its own - the card
   already provides the white ground it has to sit on. */
.auth-brand {
    display: flex;
    justify-content: center;
    padding-bottom: var(--space-5);
    border-bottom: 1px solid var(--border-subtle);
}
.auth-brand img { width: 190px; height: 56px; object-fit: contain; }
.auth-card h1 { font-size: var(--text-xl); text-align: center; }
.auth-card > p { color: var(--text-muted); font-size: var(--text-sm); text-align: center; }
.auth-colophon { font-size: var(--text-xs); color: rgba(255, 255, 255, .55); letter-spacing: .05em; text-align: center; }
.auth-header { display: flex; flex-direction: column; gap: var(--space-1); text-align: center; }
.auth-header p { color: var(--text-muted); font-size: var(--text-sm); }
.auth-form { display: flex; flex-direction: column; gap: var(--space-4); }
.auth-form .primary-button { width: 100%; min-height: 40px; }
.auth-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-4);
    flex-wrap: wrap;
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-subtle);
    font-size: var(--text-sm);
    color: var(--text-muted);
}
.auth-language { display: flex; justify-content: center; }
.auth-language .language-switcher { background: rgba(255, 255, 255, .12); border-color: rgba(255, 255, 255, .2); }
.auth-language .language-switcher button { color: rgba(255, 255, 255, .75); }
.auth-language .language-switcher button.active { background: rgba(255, 255, 255, .95); color: var(--pm-navy-800); }

.password-input { position: relative; display: block; }
.password-input input { padding-right: 42px; }
.password-visibility {
    position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
    display: grid; place-items: center;
    width: 28px; height: 28px;
    border: 0; border-radius: var(--radius-sm);
    background: none; color: var(--text-muted); cursor: pointer;
}
.password-visibility:hover { background: var(--surface-hover); color: var(--text-default); }
.password-visibility svg { width: 18px; height: 18px; }
.password-visibility[aria-pressed="true"] { color: var(--pm-navy-700); }
.password-strength {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: 16px;
}
.password-strength .strength-bars { display: flex; gap: 3px; flex: 1; }
.password-strength .strength-bars i {
    flex: 1;
    height: 4px;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    transition: background var(--duration-fast) var(--ease-out);
}
.password-strength .strength-bars i.is-on { background: var(--success-500); }
.password-strength .strength-label {
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    color: var(--text-muted);
    min-width: 7ch;
    text-align: right;
}
.password-strength.is-weak .strength-bars i.is-on { background: var(--danger-500); }
.password-strength.is-weak .strength-label { color: var(--danger-600); }
.password-strength.is-medium .strength-bars i.is-on { background: var(--warning-500); }
.password-strength.is-medium .strength-label { color: var(--warning-600); }
.password-strength.is-strong .strength-label { color: var(--success-600); }

.generated-password, .development-token {
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-3);
    border: 1px dashed var(--border-default);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
    font-size: var(--text-sm);
}
.generated-password code, .development-token code { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-strong); }

/* --------------------------------------------------------------------------
   10. Upload
   -------------------------------------------------------------------------- */
.upload-dropzone {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-7) var(--space-5);
    border: 2px dashed var(--border-default);
    border-radius: var(--radius-lg);
    background: var(--surface-sunken);
    text-align: center;
    cursor: pointer;
    transition: border-color var(--duration-base) var(--ease-out), background var(--duration-base) var(--ease-out);
}
.upload-dropzone:hover, .upload-dropzone.is-dragover { border-color: var(--pm-orange-500); background: var(--pm-orange-100); }
.upload-symbol { display: grid; place-items: center; width: 48px; height: 48px; border-radius: var(--radius-pill); background: var(--surface-raised); box-shadow: var(--shadow-sm); }
.upload-symbol svg { width: 22px; height: 22px; stroke: var(--pm-navy-600); fill: none; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.upload-copy strong { display: block; color: var(--text-strong); }
.upload-copy span { font-size: var(--text-sm); color: var(--text-muted); }
.upload-format-row { display: flex; gap: var(--space-2); flex-wrap: wrap; justify-content: center; }

/* --------------------------------------------------------------------------
   11. Image preview grid
   -------------------------------------------------------------------------- */
.image-preview-grid { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); }
.image-preview-tile, .psf-image-choice {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: 0;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
    overflow: hidden;
    cursor: pointer;
    transition: border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.image-preview-tile:hover, .psf-image-choice:hover { border-color: var(--pm-orange-400); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.image-preview-tile.selected, .psf-image-choice.selected { border-color: var(--pm-orange-500); box-shadow: 0 0 0 2px var(--pm-orange-500); }
.image-preview-tile img, .psf-image-choice img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; background: var(--neutral-900); }
.image-preview-tile span, .psf-image-choice span { padding: var(--space-1) var(--space-2) var(--space-2); font-size: var(--text-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }

.psf-image-picker { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); max-height: 320px; overflow: auto; padding: var(--space-1); }

/* --------------------------------------------------------------------------
   12. Detection viewer (shared read-only image stage)
   -------------------------------------------------------------------------- */
.detection-viewer-stage, .psf-image-stage {
    position: relative;
    overflow: hidden;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--neutral-900);
    aspect-ratio: 4 / 3;
}
.detection-viewer-stage.is-panning { cursor: grabbing; }
.psf-image-wrap { position: absolute; inset: 0; transform-origin: 0 0; }
.psf-fit-image { width: 100%; height: 100%; object-fit: contain; image-rendering: pixelated; }
.psf-fit-image.inverted, .image-inverted { filter: invert(1); }

.image-canvas-detections { position: absolute; inset: 0; pointer-events: auto; }
.image-canvas-magnifier {
    position: absolute;
    width: 168px; height: 168px;
    margin: -84px 0 0 -84px;
    border: 2px solid var(--marker-selected);
    border-radius: 50%;
    background-repeat: no-repeat;
    box-shadow: var(--shadow-lg);
    pointer-events: none;
    opacity: 0;
    image-rendering: pixelated;
    transition: opacity var(--duration-fast) var(--ease-out);
}
.image-canvas-magnifier.visible { opacity: 1; }
.image-canvas-magnifier.inverted { filter: invert(1); }

.detection-viewer-toolbar {
    position: absolute;
    right: var(--space-3);
    bottom: var(--space-3);
    display: flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1);
    border-radius: var(--radius-md);
    background: rgba(12, 18, 26, .82);
    backdrop-filter: blur(6px);
}
.detection-viewer-toolbar .icon-tool-button { border-color: rgba(255, 255, 255, .18); background: transparent; color: #fff; }
.detection-viewer-toolbar .icon-tool-button:hover { background: rgba(255, 255, 255, .14); }
/* Sits in a light tool group now, not in the old dark overlay bar - white on white
   left the zoom factor invisible. */
.image-canvas-zoom-value { min-width: 52px; text-align: center; font-size: var(--text-xs); color: var(--text-default); font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------------
   13. Legacy compatibility
   Older pages still use these names; keep them mapped onto the new system
   until each page is migrated.
   -------------------------------------------------------------------------- */
.page-shell { width: 100%; max-width: var(--content-max); margin: 0 auto; padding: var(--space-6); }
.page-header { display: flex; align-items: flex-start; gap: var(--space-4); margin-bottom: var(--space-5); }
.top-nav { display: flex; flex: 1; align-items: center; gap: var(--space-4); font-size: var(--text-sm); }
.phr-title { font-size: var(--text-xl); font-weight: var(--weight-semibold); }
.pm-logo-frame { display: block; width: 145px; height: 64px; overflow: hidden; }
.pm-logo { width: 145px; height: 64px; object-fit: contain; }
.header-actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); }
.app-footer { margin-top: var(--space-6); color: var(--text-muted); font-size: var(--text-xs); }

/* A helper that belongs to a field, not to the dialog: suggesting a password is part of
   filling the field in, so it sits under it rather than among the decisions. */
.field-actions { display: flex; justify-content: flex-end; margin-top: var(--space-1); }

/* The dropzone IS the control: the whole thing is a label, so a click anywhere opens the
   file picker. The native input has to disappear - it draws its own button in the browser's
   language, which no amount of localisation reaches, and its width stretches the panel.
   Hidden this way rather than with display:none so it still takes keyboard focus. */
.upload-dropzone input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
.upload-dropzone { position: relative; }
/* Focus lands on the invisible input, so the ring has to be drawn by its container. */
.upload-dropzone:has(input[type="file"]:focus-visible) {
    outline: 2px solid var(--border-focus);
    outline-offset: 2px;
    border-color: var(--pm-orange-500);
}
.upload-formats { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
.upload-formats > small { color: var(--text-muted); text-align: center; max-width: 62ch; }

/* A confirmation inside a dialog, where the page's own notice would be hidden behind it. */
.field-help.is-success { color: var(--success-600); }
