:root {
    /* Tells the browser which native-control palette to use for anything
       CSS can't restyle directly - the date input's calendar-picker icon,
       checkbox/radio default chrome, and (absent explicit scrollbar-color/
       ::-webkit-scrollbar rules below) the page scrollbar. Without this,
       those render with their light-mode assumption baked in regardless of
       our own dark theme - e.g. a dark calendar icon on a dark input,
       invisible. */
    color-scheme: dark;

    --bg-grad-1: #0b0f2e;
    --bg-grad-2: #142454;
    --bg-grad-3: #1e2e61;
    --shell-accent-1: rgba(67, 240, 255, 0.12);
    --shell-accent-2: rgba(14, 133, 163, 0.18);

    --primary: #0e7a8f;
    --primary-dark: #0a5c6c;
    --accent: #43f0ff;

    --glass-light-bg: rgba(255, 255, 255, 0.74);
    --glass-light-bg-strong: rgba(255, 255, 255, 0.88);
    --glass-light-border: rgba(255, 255, 255, 0.55);
    --glass-dark-bg: rgba(9, 13, 38, 0.55);
    --glass-dark-bg-strong: rgba(9, 13, 38, 0.72);
    --glass-dark-border: rgba(255, 255, 255, 0.12);

    --text-invert: #eef2ff;
    --text-muted-invert: #a6b0d0;

    --danger: #dc2626;
    --success: #16a34a;
    --warning: #d97706;

    /* Categorical (project) palette for charts - validated 8-slot order
       (dataviz skill's reference palette; Comfort Portal had no existing
       chart palette to preserve). Segments are strokes that only need to
       read against each other (see the donut partial), not against
       --surface, so they stay unconditional even now that --surface itself
       is theme-aware below. Worst adjacent CVD ΔE 9.1 light / 8.4 dark,
       normal-vision floor 19.6/19.3. */
    --cat-1: #2a78d6; --cat-2: #eb6834; --cat-3: #1baf7a; --cat-4: #eda100;
    --cat-5: #e87ba4; --cat-6: #008300; --cat-7: #4a3aa7; --cat-8: #e34948;

    /* Theme-able chrome tokens. Base values below are the "dark" look (the
       app's long-standing default - see .sidebar etc.). [data-theme="light"]
       redefines these only; component rules never hardcode which theme
       they're in. */
    --sidebar-bg: var(--glass-dark-bg-strong);
    --sidebar-border: var(--glass-dark-border);
    --sidebar-text: var(--text-invert);
    --sidebar-text-muted: var(--text-muted-invert);
    --sidebar-hover-bg: rgba(255, 255, 255, 0.08);
    --header-text: #fff;
    --header-text-shadow: 0 1px 8px rgba(0, 0, 0, 0.25);

    /* Theme-able content tokens - same rule as the chrome tokens above
       (dark is the base, [data-theme="light"] redefines). Added 2026-08-21,
       revised twice the same day after live review:
       --surface/--text used to be unconditional white/near-black (a
       deliberate earlier choice - cards were meant to "always stay the
       light surface regardless of theme"), which is why dark mode used to
       only dim the shell/sidebar and leave every card glaring white.
       --hairline/--hover-tint replace what used to be hardcoded
       rgba(16,24,40,x) "dark ink on white paper" divider/hover tints
       sprinkled through the table/form/timeline rules - those need a
       light-tinted dark-mode counterpart or they vanish against a dark
       --surface. --heading-accent/--icon-badge-bg/--link exist because
       var(--primary) and var(--primary-dark) (both tuned to sit on white)
       go low-contrast on a dark card - #0e7a8f (--primary) as plain link
       text against the first attempt (#171f45) measured ~3.2:1, below the
       4.5:1 text minimum, and every <a> in the app uses it. #171f45 was
       also *darker* than the shell gradient's brightest corner (#1e2e61) -
       cards would sink into the background there instead of reading as
       raised. Landed on #343e64 ("Slate 111827+") after a 9-option mockup,
       shipped, then revised once more the same day: <select>/.filter-
       multiselect summary/text inputs didn't visually agree with each
       other or the card - select used --surface directly (needed for its
       native popup, see the `select`/`option` rule below) while regular
       inputs and the multiselect summary used a separate --input-bg
       (translucent white over --surface), so the "same-looking" controls
       rendered as 3 different shades. Fixed by moving --surface itself to
       #3c4564 - close to what --input-bg used to composite to - and
       dropping --input-bg entirely so every field (input/select/
       multiselect summary) is now literally var(--surface), same as the
       card; only --input-border marks a field as a field. Every token
       below that depends on --surface (--link, --heading-accent,
       --status-*) was re-measured against it, not just eyeballed. */
    --surface: #3c4564;
    --surface-border: rgba(255, 255, 255, 0.10);
    --text: var(--text-invert);
    --text-muted: var(--text-muted-invert);
    --link: #5accdf;
    --hairline: rgba(255, 255, 255, 0.10);
    --hover-tint: rgba(255, 255, 255, 0.06);
    --scrollbar-thumb: rgba(255, 255, 255, 0.18);
    --scrollbar-thumb-hover: rgba(255, 255, 255, 0.30);
    --input-border: rgba(255, 255, 255, 0.16);
    --disabled-bg: rgba(255, 255, 255, 0.05);
    --btn-secondary-bg: rgba(255, 255, 255, 0.08);
    --btn-secondary-bg-hover: rgba(255, 255, 255, 0.14);
    --glass-bg: var(--glass-dark-bg-strong);
    --glass-border: var(--glass-dark-border);
    --heading-accent: var(--accent);
    --icon-badge-bg: rgba(67, 240, 255, 0.14);

    /* Badge tones, dark-mode variants - unlike the light-mode pastel chips
       (pale bg + saturated text, see [data-theme="light"] below), these are
       a translucent wash of the status color over --surface plus a
       lightened version of the status hue as text. A pastel-on-white chip
       just becomes a small glowing patch on a dark card; this keeps badges
       reading as "dark chips with colored text," consistent with the rest
       of the dark UI. Each pair measured >=4.5:1 against its own
       composited background at the current --surface. */
    --status-pos: #7fe0b3; --status-pos-bg: #34545f;
    --status-info: #8fc3f5; --status-info-bg: #364a6f;
    --status-neu: #c3d1d4; --status-neu-bg: #434f67;
    --status-neg: #ff9d94; --status-neg-bg: #5d3c50;
    --status-warning: #ffc266; --status-warning-bg: #584b48;

    /* --danger/--success/--warning stay fixed brand tones (they're gradient
       stops behind white button text - see .btn-danger - which needs them
       to stay this saturated, not lighten). Anything that instead uses one
       of these as *text or a small icon/dot directly on --surface* (error
       copy, the required-field asterisk, timeline status dots) needs the
       lighter -accent variant below, reusing the same hues already
       measured for the badges above so the palette stays coherent. */
    --danger-accent: var(--status-neg);
    --success-accent: var(--status-pos);
    --warning-accent: var(--status-warning);

    --font-mono: ui-monospace, "Cascadia Code", Consolas, "SFMono-Regular", Menlo, monospace;

    /* Rendered height of a text input/select: 2×space-2 padding + a 14px/
       1.5 line-box + 2px border. Named explicitly (rather than left
       implicit) so a checkbox-only field can match it exactly instead of
       guessing a margin. */
    --control-height: 39px;

    --sidebar-width: 232px;
    --radius: 14px;
    --radius-sm: 10px;
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 16px;
    --space-4: 24px;
    --space-5: 32px;
}

:root[data-theme="light"] {
    color-scheme: light;

    --bg-grad-1: #eef4f4;
    --bg-grad-2: #e6efef;
    --bg-grad-3: #dbe7e7;
    --shell-accent-1: rgba(67, 240, 255, 0.10);
    --shell-accent-2: rgba(14, 133, 163, 0.08);

    --sidebar-bg: rgba(255, 255, 255, 0.9);
    --sidebar-border: rgba(16, 24, 40, 0.08);
    --sidebar-text: var(--text);
    --sidebar-text-muted: var(--text-muted);
    --sidebar-hover-bg: rgba(16, 24, 40, 0.05);
    --header-text: var(--text);
    --header-text-shadow: none;

    --surface: #ffffff;
    --surface-border: rgba(16, 24, 40, 0.10);
    --text: #101828;
    --text-muted: #5b6472;
    --link: var(--primary);
    --hairline: rgba(16, 24, 40, 0.08);
    --hover-tint: rgba(16, 24, 40, 0.06);
    --scrollbar-thumb: rgba(16, 24, 40, 0.18);
    --scrollbar-thumb-hover: rgba(16, 24, 40, 0.30);
    --input-border: rgba(16, 24, 40, 0.14);
    --disabled-bg: rgba(16, 24, 40, 0.06);
    --btn-secondary-bg: rgba(255, 255, 255, 0.6);
    --btn-secondary-bg-hover: rgba(255, 255, 255, 0.9);
    --glass-bg: var(--glass-light-bg-strong);
    --glass-border: var(--glass-light-border);
    --heading-accent: var(--primary-dark);
    --icon-badge-bg: rgba(14, 122, 143, 0.12);

    /* Badge tones, light-mode variants (pale bg + saturated text) - the
       original values, now scoped to this theme since dark mode needs its
       own pairing (see :root). */
    --status-pos: #1f7a52; --status-pos-bg: #e2f3ea;
    --status-info: #25588c; --status-info-bg: #e4eef7;
    --status-neu: #54696f; --status-neu-bg: #eef2f2;
    --status-neg: #b3261e; --status-neg-bg: #fbe6e4;
    --status-warning: #a15c00; --status-warning-bg: #fbead0;

    --danger-accent: var(--danger);
    --success-accent: var(--success);
    --warning-accent: var(--warning);
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
}

* { box-sizing: border-box; }

/* Form controls don't inherit the page font by default in browser UA
   stylesheets (buttons especially render in the platform's native control
   font otherwise) - without this, a <button class="btn"> has different
   line-height metrics than an <a class="btn">, so two same-size buttons
   sitting side by side (e.g. "Upravit"/"Smazat") come out visibly
   different heights despite identical padding. */
button, input, select, textarea {
    font-family: inherit;
    font-size: 100%;
    line-height: inherit;
}

/* overscroll-behavior-y: none stops the page-edge rubber-band bounce -
   without it, reaching the bottom of a tall page (dashboard, settings)
   lets scrollY briefly exceed the real max scroll, which pushes the sticky
   .sidebar (height: 100vh, constrained to .app-shell's bounds - see its
   comment) past its container and makes it visibly hop by a few px,
   dragging .sidebar-user's logout icon with it. Short pages never scroll
   far enough to trigger the bounce, which is why it only shows there. */
html, body {
    min-height: 100%;
    overscroll-behavior-y: none;
}

/* Scrollbar - color-scheme above already gets the browser's *default* dark
   scrollbar on the page, but that default doesn't know about our palette
   and every other scroll container in the app (.sidebar, .timeline-scroll,
   .filter-multiselect-panel, .onboard-row-results, .table-wrap) still
   needs its own thumb color, so this is themed explicitly rather than left
   to the default. scrollbar-width/scrollbar-color is the standard (Firefox
   today); the ::-webkit-scrollbar rules are the Chromium/Safari
   equivalent, both needed since neither engine supports the other's API.
   Applied to * (not just html) since it's inherited per-element in
   Chromium, not globally like Firefox's version. */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--scrollbar-thumb) transparent;
}
*::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}
*::-webkit-scrollbar-track {
    background: transparent;
}
*::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb);
    border-radius: 999px;
    border: 2px solid transparent;
    background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover {
    background: var(--scrollbar-thumb-hover);
    background-clip: content-box;
}

body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    color: var(--text);
    font-size: 14px;
    line-height: 1.5;
    background:
        radial-gradient(circle at 15% 10%, var(--shell-accent-1), transparent 45%),
        radial-gradient(circle at 85% 90%, var(--shell-accent-2), transparent 50%),
        linear-gradient(135deg, var(--bg-grad-1) 0%, var(--bg-grad-2) 55%, var(--bg-grad-3) 100%);
    background-attachment: fixed;
    transition: background-color 0.2s ease;
}

a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }

.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 2000;
    background: var(--primary);
    color: #fff;
    font-weight: 600;
    padding: 10px 18px;
    border-radius: 0 0 var(--radius-sm) 0;
}
.skip-link:focus { left: 0; }

/* Icons - inline SVG sprite (assets/icons/sprite.svg), referenced via
   View::icon(). Icons take their color from the surrounding text
   (stroke: currentColor) so they follow theme/button/link color for free. */
.icon {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    vertical-align: -0.15em;
}

/* Layout */
.app-shell {
    display: flex;
    min-height: 100vh;
}

/* position: sticky + height: 100vh keeps the sidebar pinned to the
   viewport regardless of how tall .main-content grows - without it, the
   sidebar (a plain flex item) grows to match .app-shell's height on a long
   page, so .sidebar-user (pinned to the sidebar's own bottom via
   .sidebar-nav's flex-grow: 1) ends up far down the page instead of
   visible near the bottom of the screen while scrolling. overflow-y: auto
   is a safety net if the nav itself ever gets taller than the viewport. */
.sidebar {
    width: var(--sidebar-width);
    flex-shrink: 0;
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
    background: var(--sidebar-bg);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-right: 1px solid var(--sidebar-border);
    color: var(--sidebar-text);
    display: flex;
    flex-direction: column;
    padding: var(--space-4) var(--space-3);
    transition: background-color 0.2s ease, border-color 0.2s ease;
}

.sidebar-brand {
    font-weight: 700;
    font-size: 18px;
    line-height: 1;
    letter-spacing: 0.01em;
    min-height: 41px;
    margin-bottom: var(--space-4);
    color: var(--sidebar-text);
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.sidebar-brand::before {
    content: "";
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: linear-gradient(135deg, var(--primary), var(--accent));
    box-shadow: 0 0 12px var(--accent);
}

.sidebar-nav {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    flex-grow: 1;
}

/* Groups links by function (overview / licenses+equipment / people+projects).
   .sidebar-nav's larger gap gives groups breathing room, and a dotted rule
   in-between marks the boundary explicitly. */
.nav-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.nav-group + .nav-group {
    padding-top: var(--space-3);
    border-top: 1px dotted var(--sidebar-border);
}

.nav-link {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--sidebar-text-muted);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    font-weight: 500;
    transition: background 0.15s ease, color 0.15s ease;
}

.nav-link:hover {
    background: var(--sidebar-hover-bg);
    text-decoration: none;
    color: var(--sidebar-text);
}

.nav-link.active {
    background: linear-gradient(135deg, var(--primary), #0aa8c9);
    color: #fff;
    box-shadow: 0 1px 4px rgba(14, 133, 163, 0.35);
}

.sidebar-user {
    border-top: 1px solid var(--sidebar-border);
    padding-top: var(--space-3);
    margin-top: var(--space-3);
    font-size: 13px;
}

.sidebar-user-top { display: flex; align-items: center; gap: var(--space-2); }
.sidebar-user-name { display: flex; align-items: center; gap: 6px; color: var(--sidebar-text); font-weight: 600; }
.sidebar-user-role { color: var(--sidebar-text-muted); text-transform: capitalize; margin-bottom: var(--space-2); }

.link-button {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: none;
    color: var(--sidebar-text-muted);
    padding: 0;
    cursor: pointer;
    font-size: 13px;
    font-family: inherit;
}
.link-button:hover { color: var(--sidebar-text); text-decoration: underline; }
.icon-danger { color: var(--danger); }
.icon-pos { color: var(--success-accent); }

.theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--sidebar-border);
    background: transparent;
    color: var(--sidebar-text-muted);
    cursor: pointer;
    margin-left: auto;
}
.theme-toggle:hover { color: var(--sidebar-text); background: var(--sidebar-hover-bg); }
.theme-toggle .icon { width: 15px; height: 15px; }

.main-content {
    flex-grow: 1;
    min-width: 0;
    padding: var(--space-4) var(--space-5);
}

.auth-shell {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
}

/* Page header - min-height matches .sidebar-brand's box height (both are
   41px, the natural height of a .btn) so the sidebar nav and the page's
   content area start at the same offset below their respective headers,
   whether or not a given page happens to have a header action button. */
.page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: 41px;
    margin-bottom: var(--space-4);
}

.page-header h1 {
    margin: 0;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: 22px;
    line-height: 1;
    color: var(--header-text);
    text-shadow: var(--header-text-shadow);
}
.page-header h1 .icon { width: 22px; height: 22px; }

/* Surfaces - solid, not glass: content that sits in the main column should
   read clearly regardless of what's behind it. Glass is reserved for
   elements that genuinely float above content (toast, auth card, sidebar). */
.card, .table-wrap, .stat-card {
    background: var(--surface);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius);
    box-shadow: 0 1px 2px rgba(3, 6, 26, 0.08), 0 8px 20px rgba(3, 6, 26, 0.10);
}

.card { padding: var(--space-4); }
.card + .card { margin-top: var(--space-3); }

.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

/* display:flex + margin-top:auto on .stat-label pins the label to the
   card's bottom edge regardless of how many lines precede it (value only,
   or value + a stat-sub line) - and since .card-grid items stretch to the
   tallest sibling by default, every card in the row ends up the same
   height with its label aligned on the same baseline. */
.stat-card { padding: var(--space-3); position: relative; display: flex; flex-direction: column; }
.stat-card .icon { position: absolute; top: var(--space-3); right: var(--space-3); width: 3em; height: 3em; color: var(--link); opacity: .6; }
.stat-card .stat-value {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 28px;
    font-weight: 700;
    color: var(--heading-accent);
}
.stat-card .stat-label { color: var(--text-muted); font-size: 13px; margin-top: auto; padding-top: var(--space-2); }
.stat-card .stat-sub { color: var(--text-muted); font-size: 11.5px; margin-top: 1px; }

/* .stat-card/.stat-row are now clickable (dashboard links to the matching
   filtered list) - reset link defaults and add a hover affordance so it
   reads as a real link, not just a color-inverted card. */
a.stat-card { text-decoration: none; color: inherit; transition: border-color .12s ease, box-shadow .12s ease; }
a.stat-card:hover { border-color: var(--primary); box-shadow: 0 1px 2px rgba(3, 6, 26, 0.08), 0 10px 24px rgba(3, 6, 26, 0.16); }
a.stat-row { text-decoration: none; color: inherit; }
a.stat-row:hover { background: var(--hover-tint); }

/* Summary card - a bigger sibling of .stat-card for a module's overview
   (e.g. dashboard "Licence"/"Vybavení"): a dominant right-aligned icon on
   the left, a stacked list of label/value rows on the right. Icon color is
   the theme-invariant --primary, which holds enough contrast against
   --surface in both themes without extra rules - unlike --primary-dark,
   which needed --heading-accent (see :root) once --surface went dark. */
.summary-card {
    display: flex;
    gap: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius);
    box-shadow: 0 1px 2px rgba(3, 6, 26, 0.08), 0 8px 20px rgba(3, 6, 26, 0.10);
    padding: var(--space-4);
}
.summary-icon-col {
    flex: 0 0 30%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-between;
}
.summary-icon-badge {
    width: 110px;
    height: 110px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    background: var(--icon-badge-bg);
}
.summary-icon-badge .icon { width: 60px; height: 60px; stroke-width: 1.75; color: var(--link); }
.summary-title { font-weight: 700; font-size: 17px; text-align: center; margin-top: var(--space-2); }
.summary-stats-col { flex: 1 1 70%; display: flex; flex-direction: column; justify-content: center; }
.stat-row {
    display: flex;
    justify-content: space-between;
    padding: 6px 0;
    border-top: 1px solid var(--surface-border);
    font-size: 13.5px;
}
.stat-row:first-child { border-top: none; }
.stat-row .num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 600; }
.stat-row.total { font-weight: 700; }

/* Generic two-column row (summary cards, "K přiřazení" pools, donuts). */
.two-col { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-5); margin-bottom: var(--space-3); }

/* Small uppercase sub-heading, e.g. inside a two-col split within a .card
   ("Dostupné licence" / "Vybavení skladem"). Sits on --surface, so the
   theme-aware --text-muted (tuned to whatever --surface currently is) is
   correct here - see .page-group-label below for the shell-background
   equivalent. */
.group-label { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted); margin: 0 0 var(--space-2); }

/* Same look, for a heading sitting directly on the shell background
   instead of inside a .card (e.g. dashboard "Organizace", "Licence &
   vybavení") - uses --sidebar-text-muted rather than --text-muted since
   this sits on the shell gradient, not --surface (they resolve to the same
   value today, but tuning either one for its own surface later shouldn't
   silently detune the other) - and has its own margin-top for breathing
   room above the previous section. A
   plain .group-label picking up this margin-top too was the bug: it also
   matches the nested sub-headings above, stacking an unwanted gap inside
   their own card. */
.page-group-label { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--sidebar-text-muted); margin: var(--space-4) 0 var(--space-2); }

/* "K přiřazení" pool lists on the dashboard. */
.pool-list { list-style: none; margin: 0; padding: 0; }
.pool-list li { display: flex; justify-content: space-between; align-items: center; padding: 7px 0; border-top: 1px solid var(--surface-border); font-size: 13.5px; }
.pool-list li:first-child { border-top: none; }
.pool-list .meta { color: var(--text-muted); font-size: 12px; }
.pool-list a.act { font-size: 12.5px; font-weight: 600; white-space: nowrap; }
.card-foot-link { display: block; text-align: right; font-size: 12.5px; font-weight: 600; margin-top: var(--space-2); }

/* Donut charts (project share) - one per side of a .two-col row, each with
   its own legend beneath it (separate per-donut legends, not one shared
   legend combining both measures into a confusing "X / Y" pair - see
   DashboardController::legendRows()). */
.donut-block { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
.donut-block h3 { font-size: 12.5px; font-weight: 600; color: var(--text-muted); margin: 0; text-transform: uppercase; letter-spacing: .03em; }
.donut-center-value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 22px; font-weight: 700; fill: var(--text); }
.donut-center-label { font-size: 9.5px; fill: var(--text-muted); }
.donut-seg { transition: opacity .12s ease; }
.donut-seg:hover { opacity: .8; }
/* A per-donut legend is a plain vertical list, centered under the donut
   (inheriting .donut-block's align-items: center) - not the multi-column
   auto-fit grid this used when it was one wide legend shared below both
   donuts. That grid stretched each row's .lval (margin-left: auto) across a
   whole donut-block-wide column, leaving the value looking disconnected
   from its label. */
.legend { list-style: none; margin: var(--space-3) 0 0; padding: 0; width: 100%; display: flex; flex-direction: column; gap: 6px; }
.legend li { display: flex; align-items: center; gap: 7px; font-size: 12.5px; }
.legend .swatch { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }
.legend .lval { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--text-muted); margin-left: auto; flex-shrink: 0; }
/* Long project names truncate to one line with an ellipsis instead of
   wrapping the legend onto a second line per entry - min-width: 0 is
   required for text-overflow to actually engage inside a flex child. Full
   name available via the title attribute on hover. */
.legend .lname { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Donut+legend column specifically (not every .two-col use, e.g. the "K
   přiřazení" pools or activity timelines below on this same page): give each
   donut+legend stack meaningfully more room than the generic auto-fit
   minmax(220px,1fr) two-col default, so the legend can afford a wider
   max-width for the one-line-per-entry treatment below - donut stays on top
   and centered, legend stays stacked underneath it, just both wider than
   before. Side-by-side (donut left, legend right) was tried and rejected -
   it shrank the donut's visual weight relative to the legend, which wasn't
   the intent; the extra width belongs to the legend column, not to
   rearranging the donut. */
@media (min-width: 900px) {
    .donut-row { grid-template-columns: repeat(2, minmax(380px, 1fr)); }
}
.donut-row .legend { max-width: 340px; }

/* Protocol detail page: main content (WP sections) + a narrower info
   sidebar (status/due date/participant confirmation state) - collapses to
   one column on the same breakpoint the rest of the app already uses for
   its narrower layouts. */
.protocol-layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: var(--space-4); align-items: start; }
@media (max-width: 960px) {
    .protocol-layout { grid-template-columns: 1fr; }
}
.protocol-sidebar .participant-list { list-style: none; margin: 0; padding: 0; }
.protocol-sidebar .participant-list li { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; border-top: 1px solid var(--hairline); font-size: 13.5px; }
.protocol-sidebar .participant-list li:first-child { border-top: none; }
.protocol-sidebar .participant-list .icon-pos { color: var(--success-accent); }
.protocol-sidebar .participant-list .icon-muted { color: var(--text-muted); }

/* Table */
.table-wrap { overflow-x: auto; overflow-y: hidden; }

table { width: 100%; border-collapse: collapse; }

th, td {
    text-align: left;
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--hairline);
    vertical-align: middle;
}

/* Sortable column header link - inherits the <th>'s own muted/uppercase
   styling by default, and picks up full-strength text color + weight only
   once it's the active sort, so the header row doesn't turn into a wall of
   equally-weighted links. */
.sort-link { color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 2px; }
.sort-link:hover { text-decoration: underline; }
.sort-link.active { color: var(--text); font-weight: 700; }

th {
    background: rgba(14, 133, 163, 0.06);
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

tbody tr:hover { background: rgba(14, 133, 163, 0.06); }
tbody tr:last-child td { border-bottom: none; }

.empty-state {
    padding: var(--space-5);
    text-align: center;
    color: var(--text-muted);
}

/* Forms */
.form-group { margin-bottom: var(--space-3); }

label {
    display: block;
    font-weight: 600;
    margin-bottom: var(--space-1);
    font-size: 13px;
    color: var(--text);
}

.required { color: var(--danger-accent); font-weight: 700; }
.label-hint { color: var(--text-muted); font-weight: 400; font-size: 12px; }

input[type="text"], input[type="email"], input[type="password"],
input[type="date"], input[type="month"], input[type="number"], select, textarea {
    width: 100%;
    min-height: var(--control-height);
    padding: var(--space-2) 10px;
    border: 1px solid var(--input-border);
    border-radius: var(--radius-sm);
    font-size: 14px;
    font-family: inherit;
    background: var(--surface);
    color: var(--text);
}

/* date/month pickers carry their own intrinsic content (the calendar icon +
   its padding) that can push them taller than min-height alone accounts
   for, so they end up visibly taller than a plain text/number input right
   next to them - pin the exact height instead of just a minimum. */
input[type="date"], input[type="month"] {
    height: var(--control-height);
    box-sizing: border-box;
}

/* Firefox dims placeholders to 54% opacity by default, Chromium doesn't -
   forcing opacity: 1 and picking the color ourselves (var(--text-muted),
   already theme-aware) keeps placeholder dimness consistent across
   browsers instead of doubly-dimmed in one of them. */
::placeholder {
    color: var(--text-muted);
    opacity: 1;
}

input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(67, 240, 255, 0.35);
}

input:disabled, select:disabled, textarea:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background: var(--disabled-bg);
}

/* select's own popup list is native OS/browser chrome, not part of our DOM
   box - <option> isn't covered by the shared input/select/textarea rule
   above, so it needs its own explicit background/color or Chromium falls
   back to compositing the popup unpredictably (it doesn't have --surface
   behind it to blend with, since it's not actually nested in the page). */
option {
    background: var(--surface);
    color: var(--text);
}

textarea { min-height: 90px; resize: vertical; }

input[type="checkbox"], input[type="radio"] {
    width: 17px;
    height: 17px;
    margin: 0;
    accent-color: var(--primary);
    cursor: pointer;
    vertical-align: middle;
}

/* A checkbox-only field sitting in a .form-row next to labelled
   inputs/selects has no <label> of its own to push it down to where
   the sibling inputs start, and no input to give it their height - so
   without help it neither starts nor centers at the same place they do.
   .form-group-spacer reserves exactly a label's height (same font-size/
   margin, so it's identical by construction, not a guessed margin), and
   .checkbox-row is set to --control-height so its content centers
   against the same band a real input/select occupies. */
.form-group-spacer {
    display: block;
    font-size: 13px;
    margin-bottom: var(--space-1);
}

.checkbox-row {
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: var(--control-height);
}

.form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--space-3);
}

.form-actions {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-4);
}

/* Read-only key/value pairs on detail pages - replaces one-off
   text-muted + inline font-size divs that used to repeat on every show page. */
dl.detail {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--space-3) var(--space-4);
    margin: 0;
}
dl.detail dt { font-size: 12px; color: var(--text-muted); margin: 0 0 2px; }
dl.detail dd { margin: 0; font-size: 14px; }

/* Spacing/utility - replaces the repeated inline style="margin-top:..." */
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mb-4 { margin-bottom: var(--space-4); }
.text-xs { font-size: 12px; }
.hidden { display: none; }
.btn-block { width: 100%; }
.h2-sm { font-size: 15px; }

/* Buttons */
.btn {
    display: inline-flex;
    align-items: center;
    /* inline-flex defaults to vertical-align: baseline, same as a plain
       <a>. The <form> wrapping a table-row delete button is inline-block
       and was explicitly set to vertical-align: middle - two different
       alignment references sitting on the same line, so the two buttons
       never lined up. Both now anchor to the same reference. */
    vertical-align: middle;
    gap: 6px;
    padding: 9px 16px;
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    background: linear-gradient(135deg, var(--primary), var(--primary-dark));
    color: #fff;
    /* Tight, symmetric glow - a wide "0 4px 14px" shadow made the button
       visually taller than .btn-secondary (no shadow) and bled onto
       adjacent table borders. No transform on hover - it read as the
       button jumping rather than a focus/hover cue. */
    box-shadow: 0 1px 4px rgba(14, 133, 163, 0.3);
    transition: box-shadow 0.15s ease;
}
.btn:hover { text-decoration: none; box-shadow: 0 2px 7px rgba(14, 133, 163, 0.4); }

.btn-secondary {
    background: var(--btn-secondary-bg);
    color: var(--text);
    border-color: var(--input-border);
    box-shadow: none;
}
.btn-secondary:hover { background: var(--btn-secondary-bg-hover); box-shadow: none; }

.btn-danger { background: linear-gradient(135deg, #ef4444, var(--danger)); box-shadow: 0 1px 4px rgba(220, 38, 38, 0.25); }
.btn-danger:hover { box-shadow: 0 2px 7px rgba(220, 38, 38, 0.35); }

.btn-sm { padding: 5px 12px; font-size: 13px; }
.btn-sm .icon { width: 14px; height: 14px; }

/* No display:flex here on purpose - it was overriding the <td>'s
   table-cell display, which threw off border-collapse and made the row's
   bottom border misalign under this column specifically. Buttons are
   inline-flex already, so plain inline flow + margin gives the same
   visual row without breaking the table model. */
.actions-cell { white-space: nowrap; }
.actions-cell > * + * { margin-left: var(--space-2); }
.actions-cell form { display: inline-block; vertical-align: middle; }

/* "Naposledy přidáno" list under a create form. */
.recent-created-list { list-style: none; margin: 0; padding: 0; font-size: 13px; }
.recent-created-list li { padding: var(--space-1) 0; border-top: 1px solid var(--hairline); }
.recent-created-list li:first-child { border-top: none; }

/* Index-page search/filter row above a table. Each field is a small icon
   (identifying what that control filters on) beside the actual input/
   select, rather than an icon overlaid inside the control - native
   <select> doesn't support that without replacing it with a custom
   dropdown, which is more machinery than this needs. */
.filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}
.filter-field { display: flex; align-items: center; gap: 6px; flex: 1 1 200px; min-width: 0; }
.filter-field-wide { flex: 2 1 400px; }
/* A filter-bar with only one control (Projekty/Uživatelé's single status
   select) would otherwise have that field's flex-grow:1 stretch it to fill
   the whole remaining row width next to the buttons - this caps it back to
   a normal select-sized field. */
.filter-field-narrow { flex: 0 1 220px; }
/* Sits directly on the shell background, not a .card, so like
   .page-group-label this uses --sidebar-text-muted rather than
   --text-muted (see that comment for why the two are kept separate). */
.filter-field .icon { flex-shrink: 0; width: 16px; height: 16px; color: var(--sidebar-text-muted); }
.filter-field input[type="text"] { width: auto; flex: 1 1 auto; min-width: 0; }
.filter-field input[type="date"] { width: auto; flex: 1 1 auto; min-width: 0; }
.filter-field select { width: auto; flex: 1 1 auto; min-width: 0; }

/* Checkbox-list dropdown for a multiselect filter, built on native
   <details>/<summary> instead of a JS combobox - no build step in this app
   to pull in a component library, and <details> gives free click-to-toggle
   behavior. Structured the same as every other .filter-field (icon sibling
   + control), with <details> standing in for the select - that's what lets
   <summary> inherit the same width/flex behavior as a real <select> instead
   of shrink-wrapping its own text (which read as a small button floating in
   the middle of its allotted flex space). The panel is positioned absolute
   so it doesn't push sibling filter fields around when open. */
.filter-multiselect { position: relative; }
.filter-multiselect > details { flex: 1 1 auto; min-width: 0; }
.filter-multiselect summary {
    display: block;
    min-height: var(--control-height);
    line-height: calc(var(--control-height) - 2px);
    padding: 0 10px;
    border: 1px solid var(--input-border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font-size: 14px;
    cursor: pointer;
    list-style: none;
    user-select: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.filter-multiselect summary::-webkit-details-marker { display: none; }
.filter-multiselect details[open] summary { border-color: var(--primary); }
.filter-multiselect-panel {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    z-index: 20;
    min-width: 200px;
    max-height: 320px;
    overflow-y: auto;
    background: var(--surface);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-sm);
    box-shadow: 0 8px 20px rgba(3, 6, 26, 0.18);
    padding: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* Injected by initFilterSearch() (assets/js/app.js) only for panels with
   more than its option-count threshold - most filters (Stav, Entita, Akce)
   stay exactly as before, untouched. Sits above the checkbox list, inside
   the same scrolling panel, so it scrolls out of view with the rest rather
   than pinning awkwardly while a long list scrolls under it. */
.filter-multiselect-search {
    min-height: 30px;
    padding: 4px 8px;
    margin-bottom: 4px;
    font-size: 13px;
}
.filter-multiselect-panel label {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13.5px;
    font-weight: 400;
    padding: 5px 6px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    white-space: nowrap;
}
.filter-multiselect-panel label:hover { background: var(--hover-tint); }
.filter-multiselect-panel input[type="checkbox"] { margin: 0; }

/* Cross-filter constraint (Entita <-> Akce): an option that can't co-occur
   with what's checked on the other side is dimmed and inert, not just
   visually muted - prevents building a filter combo that can only ever
   return zero rows. */
.filter-multiselect-panel label.is-disabled {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
}

/* Pagination below a table - numbered links + prev/next, active page
   styled like a pressed .btn-secondary rather than a new visual language. */
/* margin on all 4 sides, not just top - inside .table-wrap (which has zero
   padding, so the table can run edge-to-edge under the card's rounded
   corners) pagination previously only had breathing room above it and sat
   flush against the card's left/right/bottom edges. */
.pagination {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin: var(--space-3);
}
.pagination a {
    display: inline-block;
    min-width: var(--control-height);
    padding: 6px 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--input-border);
    text-align: center;
    font-size: 13px;
    color: var(--text);
}
.pagination a:hover { background: var(--hover-tint); text-decoration: none; }
.pagination a.active {
    background: linear-gradient(135deg, var(--primary), var(--primary-dark));
    color: #fff;
    border-color: transparent;
}

/* Pagination + "records per page" share one row below the table - the
   per-page control uses margin-left: auto so it stays pinned to the right
   edge even when pagination.php renders nothing (a single page of
   results). */
.list-footer { display: flex; align-items: center; flex-wrap: wrap; }
.per-page-form { margin: var(--space-3); margin-left: auto; }
.per-page-label { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-muted); white-space: nowrap; }
/* Padding matched to .pagination a's (6px 10px, same font-size, same 1px
   border) so both controls in the same footer row come out the same
   rendered height (33.5px) - the generic <select> rule's
   min-height: var(--control-height) (39px) would otherwise win and make
   this one taller than its neighbor, so it's overridden back to auto. */
.per-page-label select { width: auto; min-height: auto; padding: 6px 10px; font-size: 13px; }

/* Generic <details>-based dropdown menu (currently just a table's "Export"
   button, sitting in the last, otherwise-unused <th> so it doesn't add a
   row and grow the table), built the same way as .filter-multiselect -
   native <details>/<summary>, no JS combobox library. Panel opens
   right-aligned (right: 0, not left: 0 like .filter-multiselect-panel)
   since this button sits at the table's right edge and a left-aligned panel
   would overflow past it. */
.dropdown { position: relative; display: inline-block; }
.dropdown > details > summary {
    list-style: none;
    cursor: pointer;
}
.dropdown > details > summary::-webkit-details-marker { display: none; }
/* A <th>'s own uppercase/letter-spacing/bold styling (bold is the browser's
   default UA style for <th>, not something this stylesheet sets - so it's
   not visible anywhere else that would already reset it) would otherwise
   inherit into both the button label and the panel's menu items sitting
   inside it - .btn's own font-weight:600 masks the bold on the summary, but
   the plain <a> menu items have no font-weight of their own to do the same. */
th .dropdown .btn,
th .dropdown-panel a {
    text-transform: none;
    letter-spacing: normal;
    font-weight: 400;
}
.dropdown-panel {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    z-index: 20;
    min-width: 200px;
    background: var(--surface);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-sm);
    box-shadow: 0 8px 20px rgba(3, 6, 26, 0.18);
    padding: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.dropdown-panel a {
    display: block;
    padding: 7px 10px;
    border-radius: var(--radius-sm);
    color: var(--text);
    text-decoration: none;
    font-size: 13.5px;
    white-space: nowrap;
}
.dropdown-panel a:hover { background: var(--hover-tint); text-decoration: none; }

/* Badges - four semantic tones (positive/info/neutral/negative), assigned
   by meaning via View::statusTone()/View::badge(), never by the literal
   DB value. A status with no explicit tone falls back to neutral, not to
   an undefined class. */
.badge {
    display: inline-block;
    padding: 3px 10px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 600;
    background: var(--status-neu-bg);
    color: var(--status-neu);
}

.badge-pos { background: var(--status-pos-bg); color: var(--status-pos); }
.badge-info { background: var(--status-info-bg); color: var(--status-info); }
.badge-neu { background: var(--status-neu-bg); color: var(--status-neu); }
.badge-neg { background: var(--status-neg-bg); color: var(--status-neg); }
.badge-warning { background: var(--status-warning-bg); color: var(--status-warning); }

/* Auth */
.auth-card {
    width: 100%;
    max-width: 380px;
    background: var(--glass-bg);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius);
    padding: var(--space-5);
    box-shadow: 0 20px 60px rgba(3, 6, 26, 0.45), 0 0 60px rgba(67, 240, 255, 0.12);
}

.auth-card h1 {
    font-size: 22px;
    margin-top: 0;
    margin-bottom: var(--space-4);
    text-align: center;
    color: var(--heading-accent);
}

.text-muted { color: var(--text-muted); }
.mt-0 { margin-top: 0; }

/* Toasts */
.toast-container {
    position: fixed;
    top: var(--space-4);
    right: var(--space-4);
    z-index: 1000;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    max-width: 360px;
}

.toast {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 12px 14px;
    border-radius: var(--radius-sm);
    background: var(--glass-bg);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border: 1px solid var(--glass-border);
    border-left: 4px solid var(--primary);
    box-shadow: 0 12px 32px rgba(3, 6, 26, 0.35);
    color: var(--text);
    font-size: 14px;
    opacity: 0;
    transform: translateX(24px);
    transition: opacity 0.25s ease, transform 0.25s ease;
}

.toast-visible { opacity: 1; transform: translateX(0); }

.toast-icon { flex-shrink: 0; }
.toast-success { border-left-color: var(--success-accent); }
.toast-success .toast-icon { color: var(--success-accent); }
.toast-error { border-left-color: var(--danger-accent); }
.toast-error .toast-icon { color: var(--danger-accent); }

.toast-message { flex-grow: 1; }

.toast-action {
    font-weight: 600;
    color: var(--link);
    white-space: nowrap;
}

.toast-close {
    display: inline-flex;
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    padding: 0;
}
.toast-close:hover { color: var(--text); }
.toast-close .icon { width: 14px; height: 14px; }

/* Modal - native <dialog> styled to match the app instead of a system
   confirm(); ::backdrop is the click-outside-to-close surface. */
dialog.modal {
    padding: 0;
    border: none;
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--text);
    box-shadow: 0 8px 30px rgba(3, 6, 26, 0.3);
    width: min(420px, calc(100vw - var(--space-4) * 2));
}
dialog.modal::backdrop { background: rgba(3, 6, 26, 0.55); }
dialog.modal .modal-body { padding: var(--space-4); }
dialog.modal h3 { margin: 0 0 var(--space-3); font-size: 17px; }

.checkout-error {
    display: none;
    color: var(--danger-accent);
    font-size: 12px;
    margin-top: var(--space-1);
}
.checkout-error.is-visible { display: block; }

/* Timeline */
.timeline {
    position: relative;
    padding-left: 22px;
}

/* Bounds a timeline column to a fixed height so two side-by-side timelines
   with very different content (e.g. one has expandable diff detail, the
   other doesn't) still occupy the same visual footprint instead of one
   looking cut short next to the other. */
.timeline-scroll { max-height: 420px; overflow-y: auto; padding-right: var(--space-2); }

.timeline::before {
    content: "";
    position: absolute;
    left: 5px;
    top: 6px;
    bottom: 6px;
    width: 2px;
    background: var(--hairline);
}

.timeline-item {
    position: relative;
    padding-bottom: var(--space-3);
}
.timeline-item:last-child { padding-bottom: 0; }

.timeline-dot {
    position: absolute;
    left: -22px;
    top: 4px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--text-muted);
    box-shadow: 0 0 0 3px var(--surface);
}

.timeline-dot-primary { background: var(--link); }
.timeline-dot-success { background: var(--success-accent); }
.timeline-dot-warning { background: var(--warning-accent); }
.timeline-dot-danger { background: var(--danger-accent); }

.timeline-message { font-size: 14px; }
.timeline-meta { font-size: 12px; color: var(--text-muted); margin-top: 2px; }

.timeline-diff {
    list-style: none;
    margin: 4px 0 0;
    padding: 6px 10px;
    background: var(--hover-tint);
    border-radius: var(--radius-sm);
    font-size: 12.5px;
}
.timeline-diff li { padding: 1px 0; color: var(--text-muted); }
.timeline-diff strong { color: var(--text); font-weight: 600; }

.btn:disabled, .btn:disabled:hover {
    opacity: 0.5;
    cursor: not-allowed;
    box-shadow: none;
}

/* Nástup's dynamic row-per-item picker (views/users/onboard.php,
   initOnboardingRows() in app.js). Each row is a search input with a
   client-side-filtered results panel - same visual language as
   .filter-multiselect-panel (absolute panel, surface background, shadow)
   since both are "type to narrow a list of options" affordances. */
.onboard-rows { display: flex; flex-direction: column; gap: var(--space-2); }
.onboard-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
}
.onboard-row-search { position: relative; flex: 2 1 360px; min-width: 0; }
.onboard-row-note { flex: 1 1 220px; min-width: 0; }
/* Matches the row's input min-height so the icon-only remove button sits in
   the same band as the search/note fields instead of looking squashed
   against align-items: flex-start (buttons have no min-height by default,
   unlike inputs - see .btn). */
.onboard-row-remove { flex-shrink: 0; min-height: var(--control-height); }

.onboard-row-results {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    z-index: 20;
    max-height: 260px;
    overflow-y: auto;
    background: var(--surface);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-sm);
    box-shadow: 0 8px 20px rgba(3, 6, 26, 0.18);
    padding: var(--space-1);
    display: flex;
    flex-direction: column;
    gap: 2px;
}
/* .hidden (display: none) is defined earlier in this file at equal
   specificity - without this compound override, .onboard-row-results'
   own `display: flex` above wins by source order and the empty panel
   renders as a permanently-visible bordered sliver under the input, even
   while "hidden". Same class of bug as the earlier sidebar/media-query
   cascade issue; fixed here with specificity instead of reordering, since
   this is a stateful toggle rather than a responsive override. */
.onboard-row-results.hidden { display: none; }
.onboard-row-result {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 6px 8px;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text);
    font-size: 13.5px;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
}
.onboard-row-result:hover { background: var(--hover-tint); }
.onboard-row-result .icon { flex-shrink: 0; width: 15px; height: 15px; color: var(--text-muted); }
.onboard-row-result-empty { color: var(--text-muted); cursor: default; }
.onboard-row-result-empty:hover { background: none; }

/* Handover protocol (views/onboarding_batches/show.php) - signature lines
   only make sense on the printed copy, and .no-print/@media print hide the
   app chrome so the printout is just the document itself. */
.protocol-signatures {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--space-5);
}
.protocol-signature-line {
    border-top: 1px solid rgba(16, 24, 40, 0.3);
    padding-top: var(--space-1);
    font-size: 12px;
    color: var(--text-muted);
    text-align: center;
}

/* Manual / support page (views/support/index.php) - a sticky in-page TOC
   beside a scrolling column of chapter cards, same two-column shape as
   .protocol-layout (narrow side column + wide content), just mirrored
   (nav on the left here) and without a fixed pixel width so long chapter
   titles can still wrap. */
.manual-layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: var(--space-4); align-items: start; }
@media (max-width: 960px) {
    .manual-layout { grid-template-columns: 1fr; }
}
.manual-toc { position: sticky; top: var(--space-4); max-height: calc(100vh - var(--space-4) * 2); overflow-y: auto; }
.manual-toc-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.manual-toc-link {
    display: block;
    padding: 6px 10px;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: 13px;
    line-height: 1.35;
}
.manual-toc-link:hover { background: var(--hover-tint); color: var(--text); text-decoration: none; }
.manual-toc-link.active { background: var(--icon-badge-bg); color: var(--link); font-weight: 600; }

/* Chapters are plain .card siblings - scroll-margin-top gives the anchor
   jump (and the "Nahoru" links) some breathing room above the card instead
   of butting the viewport's very edge. */
.manual-chapter { scroll-margin-top: var(--space-3); }
.manual-chapter-header { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); margin-bottom: var(--space-2); }
.manual-chapter-header h2 { margin: 0; font-size: 18px; display: flex; align-items: center; gap: var(--space-2); }
.manual-chapter-badges { display: flex; flex-wrap: wrap; gap: 6px; }
.manual-chapter > p:first-of-type { margin-top: 0; }
.manual-chapter h3 { font-size: 14.5px; margin: var(--space-4) 0 var(--space-2); }
.manual-chapter h3:first-of-type { margin-top: var(--space-3); }
.manual-chapter ul, .manual-chapter ol { margin: 0 0 var(--space-3); padding-left: 1.3em; }
.manual-chapter li { margin-bottom: 4px; }
.manual-chapter li:last-child { margin-bottom: 0; }

/* Search box - same shape as an index page's .filter-bar so it reads as
   the same "filter this list" affordance the rest of the app already
   uses, just filtering chapters instead of table rows. */
.manual-search { margin-bottom: var(--space-4); }
.manual-search .filter-field { flex: 1 1 auto; }
.manual-search-count { color: var(--text-muted); font-size: 12.5px; white-space: nowrap; }

/* Callout - a left-accent note box, same visual language as a toast
   (border-left + tinted glass) but static/inline for "worth knowing"
   asides inside a chapter. */
.callout {
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--hover-tint);
    border-left: 4px solid var(--link);
    font-size: 13.5px;
    margin: var(--space-3) 0;
}
.callout .icon { flex-shrink: 0; margin-top: 1px; color: var(--link); }
.callout-warning { border-left-color: var(--warning-accent); }
.callout-warning .icon { color: var(--warning-accent); }
.callout p { margin: 0 0 4px; }
.callout p:last-child { margin-bottom: 0; }

/* Wireframe diagrams - deliberately "blind" (dashed borders, muted grey
   fill, label text only, no real component styling) so they read as a
   schematic of a layout/flow rather than a screenshot - see the class
   comment on assets in views/support/index.php for where each is used. */
.wf { border: 1px dashed var(--input-border); border-radius: var(--radius-sm); background: var(--hover-tint); padding: var(--space-3); margin: var(--space-3) 0; }
.wf-caption { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); margin: 0 0 var(--space-2); }
.wf-box {
    border: 1px dashed var(--input-border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text-muted);
    font-size: 12px;
    text-align: center;
    padding: var(--space-2);
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 40px;
}
.wf-grid { display: grid; gap: var(--space-2); }
.wf-row { display: flex; gap: var(--space-2); }
.wf-row > .wf-box { flex: 1 1 0; }

/* Horizontal state-machine flow (protocol workflow, onboarding steps) -
   wraps on narrow viewports since it's inside the already-narrower content
   column on the ≤960px single-column manual layout. */
.wf-flow { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.wf-step {
    border: 1px dashed var(--input-border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    padding: 8px 12px;
    font-size: 12.5px;
    font-weight: 600;
    text-align: center;
}
.wf-step-auto { border-color: var(--status-info); color: var(--status-info); }
.wf-step-manual { border-color: var(--status-warning); color: var(--status-warning); }
.wf-arrow { color: var(--text-muted); font-size: 16px; flex-shrink: 0; }
.wf-arrow-label { font-size: 10.5px; color: var(--text-muted); text-align: center; flex-basis: 100%; margin-top: -6px; }

/* Responsive - kept last on purpose: these override component rules above
   (.sidebar, .app-shell, .page-header...) at equal selector specificity, so
   they must win the cascade by source order at any width where they apply. */
@media (max-width: 960px) {
    .app-shell { flex-direction: column; }

    .sidebar {
        width: 100%;
        height: auto;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-4);
        padding: var(--space-2) var(--space-3);
    }

    .sidebar-brand { margin-bottom: 0; min-height: 0; }

    .sidebar-nav {
        flex-direction: row;
        flex-wrap: wrap;
        flex-grow: 0;
        gap: var(--space-4);
    }

    .nav-group {
        flex-direction: row;
        flex-wrap: wrap;
        gap: var(--space-1);
    }

    /* Separator flips from a horizontal rule above each group to a
       vertical one to its left, since groups now flow left-to-right. */
    .nav-group + .nav-group {
        padding-top: 0;
        padding-left: var(--space-3);
        border-top: none;
        border-left: 1px dotted var(--sidebar-border);
    }

    /* The user/theme/logout block used to stay in its own stacked
       (name / role / logout) column even in this single-line top bar,
       which looked disconnected from the horizontal nav next to it. Drop
       the role label (already visible in the full sidebar) and lay the
       rest out as one row instead. align-self: stretch makes its own
       left border span the full bar height (matching the bar itself)
       instead of just its own shorter content height. */
    .sidebar-user {
        display: flex;
        align-items: center;
        align-self: stretch;
        gap: var(--space-3);
        border-top: none;
        border-left: 1px solid var(--sidebar-border);
        margin-top: 0;
        margin-left: auto;
        padding-top: 0;
        padding-left: var(--space-3);
    }

    .sidebar-user-role { display: none; }

    .main-content { padding: var(--space-3); }

    .summary-icon-badge { width: 84px; height: 84px; }
    .summary-icon-badge .icon { width: 44px; height: 44px; }
}

@media (max-width: 640px) {
    .page-header { flex-direction: column; align-items: flex-start; gap: var(--space-2); }
    .card-grid { grid-template-columns: repeat(2, 1fr); }
    .summary-icon-badge { width: 64px; height: 64px; }
    .summary-icon-badge .icon { width: 32px; height: 32px; }
}

/* Handover protocol print view (views/onboarding_batches/show.php) - hides
   the app chrome so what comes out of the printer is just the document,
   not the sidebar/toolbar around it. */
@media print {
    .sidebar, #toast-container, .skip-link, .no-print { display: none !important; }
    .app-shell { display: block; }
    .main-content { padding: 0; }
    .card { box-shadow: none; border: none; }
}

/* Quill.js (work package description, protocol activity text) - themed with
   the existing --surface/--text tokens so it doesn't need its own light/dark
   blocks. Library defaults (.ql-toolbar/.ql-container/.ql-editor etc.) come
   from assets/vendor/quill/quill.snow.css, loaded only on the two views that
   use it. */
.richtext-field .richtext-editor {
    background: var(--surface);
    border: 1px solid var(--input-border);
    border-radius: var(--radius-sm);
    overflow: hidden;
}
.richtext-editor .ql-toolbar.ql-snow { border: none; border-bottom: 1px solid var(--hairline); }
.richtext-editor .ql-container.ql-snow { border: none; }
.richtext-editor .ql-editor { min-height: 80px; font-size: 14px; color: var(--text); }
.richtext-editor .ql-editor.ql-blank::before { color: var(--text-muted); font-style: normal; }
.richtext-editor .ql-snow .ql-stroke { stroke: var(--text-muted); }
.richtext-editor .ql-snow .ql-fill { fill: var(--text-muted); }
.richtext-editor .ql-picker { color: var(--text-muted); }
.richtext-editor .ql-snow .ql-picker-options { background: var(--surface); border-color: var(--input-border); }

/* Rendered rich-text content (protocol activity list, WP description) */
.richtext-content { color: var(--text); }
.richtext-content p { margin: 0 0 .5em; }
.richtext-content p:last-child { margin-bottom: 0; }
.richtext-content ul, .richtext-content ol { margin: 0 0 .5em 1.2em; padding: 0; }

/* [hidden] must actually hide, full stop - without !important, any later
   same/higher-specificity rule that sets `display` on the same element
   (e.g. `.pool-list li { display: flex }`) wins over the attribute per
   normal CSS cascade rules, since author stylesheets load after the
   browser's own `[hidden] { display: none }` default. Bit this exact way
   2026-09: every data-toggle-block (inline edit rows, the zástup reveal)
   rendered permanently visible instead of only on demand. */
[hidden] { display: none !important; }

/* Card header with a title plus one action button (e.g. "Pracovní balíčky"
   + "Přidat") - same alignment as .page-header, scaled down for use inside
   a .card instead of at page level. */
.card-header-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-2); }
.card-header-row h2 { margin: 0; }

/* Tight icon-only button pair (protocol participant row: "Zástup" + remove) -
   input-group instead of two independently-spaced full-width buttons. */
.icon-btn-group { display: inline-flex; gap: 4px; flex-shrink: 0; align-items: center; }

/* Work package description, shown collapsed on both the project page and
   the protocol detail page. Deliberately near-invisible while collapsed -
   just the small summary line, no box - so it doesn't disrupt the row it
   sits in (the boxed/callout treatment (support manual's info asides: left
   accent stripe + icon/background) only switches on for the [open] state,
   once there's actual content to visually set apart). */
.callout-details summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--text-muted);
}
.callout-details summary::-webkit-details-marker { display: none; }
.callout-details summary .icon { color: var(--link); flex-shrink: 0; width: 14px; height: 14px; }
.callout-details[open] {
    border: 1px solid var(--input-border);
    border-left: 3px solid var(--link);
    border-radius: var(--radius-sm);
    background: var(--hover-tint);
    padding: var(--space-2) var(--space-3);
}
.callout-details[open] summary { margin-bottom: var(--space-2); color: var(--text); }
.callout-details .richtext-content { font-size: 13px; }

/* Work package description on the project page's list (views/projects/
   show.php): inline next to the name, icon-only trigger, panel positioned
   absolute (same idiom as .filter-multiselect-panel/.dropdown-panel - a
   floating overlay, not part of document flow) so opening it never shifts
   or grows the list row it's attached to, unlike .callout-details' plain
   <details> push-down (fine on the protocol detail page, where each work
   package already has its own card with nothing below to disturb; wrong
   here, where rows are packed tightly one after another). */
.wp-info-popover { position: relative; display: inline-block; vertical-align: middle; margin-left: 2px; }
.wp-info-popover > details > summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    color: var(--text-muted);
}
.wp-info-popover > details > summary::-webkit-details-marker { display: none; }
.wp-info-popover > details > summary .icon { width: 14px; height: 14px; }
.wp-info-popover > details[open] > summary { color: var(--link); }
.wp-info-popover-panel {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 20;
    min-width: 220px;
    max-width: min(360px, 80vw);
    background: var(--surface);
    border: 1px solid var(--surface-border);
    border-left: 3px solid var(--link);
    border-radius: var(--radius-sm);
    box-shadow: 0 8px 20px rgba(3, 6, 26, 0.18);
    padding: var(--space-2) var(--space-3);
    font-size: 13px;
}

/* A <ul>/<ol> from rich-text content can end up nested inside some other
   list-ish container (.pool-list here: the work package description sits
   inside a .pool-list <li>) - without this, `.pool-list li` (a descendant
   selector, so it matches ANY <li> at any depth, not just direct children)
   outranks the browser's default list-item styling and turns the rich
   text's own <li>s into flex rows with dividers instead of a bulleted/
   numbered list. Bit this exact way 2026-09: a pasted bullet list rendered
   as plain divider-separated lines, no bullets at all. */
.richtext-content ul, .richtext-content ol { list-style-position: outside; }
.richtext-content ul li, .richtext-content ol li {
    display: list-item;
    padding: 0;
    margin: 0;
    border-top: none;
    align-items: normal;
    justify-content: normal;
}

/* Equipment detail page's top card: status/dl/note on the left, photo (or
   a placeholder) on the right - see views/equipment/show.php. Wraps to a
   single column on narrow screens rather than squeezing the photo. The
   photo itself is no longer forced into a square box (crop is free-form
   now, see the crop dialog below) - max-width/max-height + width/height:
   auto lets it render at its own natural aspect ratio, just capped so it
   never dominates the card regardless of what ratio was cropped. */
.equipment-detail-row { display: flex; gap: var(--space-4); align-items: flex-start; flex-wrap: wrap; }
.equipment-detail-main { flex: 1 1 260px; min-width: 0; }
.equipment-detail-photo { flex: 0 0 auto; }
.equipment-detail-photo img {
    display: block;
    max-width: 220px;
    max-height: 180px;
    width: auto;
    height: auto;
    border-radius: var(--radius-sm);
    border: 1px solid var(--surface-border);
    cursor: zoom-in;
}
.equipment-photo-placeholder {
    width: 140px;
    height: 140px;
    border-radius: var(--radius-sm);
    border: 1px dashed var(--input-border);
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--hover-tint);
}
.equipment-photo-placeholder .icon { width: 40px; height: 40px; color: var(--text-muted); opacity: .5; }

/* Equipment create/edit form's photo field (views/equipment/form.php) -
   small preview + upload/pick/remove controls, crop dialog (Cropper.js,
   assets/vendor/cropperjs/) and "Vybrat z kolekce" picker grid. */
.equipment-photo-field { display: flex; gap: var(--space-3); align-items: flex-start; flex-wrap: wrap; }
.equipment-photo-preview {
    min-width: 110px;
    min-height: 110px;
    flex-shrink: 0;
    border-radius: var(--radius-sm);
    border: 1px solid var(--surface-border);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--hover-tint);
}
/* Shows the actual thumbnail (or the just-cropped preview, same source) at
   its own aspect ratio - crop is free-form now, forcing this box back to a
   square via object-fit: cover would visually re-crop it to a square right
   after the user deliberately chose a different ratio. */
.equipment-photo-preview img { max-width: 160px; max-height: 160px; width: auto; height: auto; display: block; }
.equipment-photo-controls { display: flex; flex-direction: column; gap: var(--space-2); min-width: 180px; }

/* Wider than the default dialog.modal (420px) - the crop UI needs real
   room to work with, a phone-sized modal makes precise dragging painful. */
.photo-crop-modal { width: min(640px, 94vw); }
.crop-ratio-buttons { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-bottom: var(--space-2); }
.crop-ratio-buttons .btn.is-active { background: var(--primary); border-color: var(--primary); color: #fff; }
.photo-crop-area { max-height: 60vh; overflow: hidden; }
.photo-crop-area img { max-width: 100%; display: block; }

/* Click-to-preview lightbox (views/equipment/show.php) - wider than the
   default dialog.modal, no padding (.modal-body isn't used here - the
   image should fill the dialog, not sit in a padded card). */
.photo-lightbox { width: min(800px, 94vw); background: transparent; box-shadow: none; }
.photo-lightbox img { display: block; width: 100%; height: auto; border-radius: var(--radius); }

.photo-picker-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
    gap: var(--space-2);
    max-height: 50vh;
    overflow-y: auto;
}
.photo-picker-item {
    padding: 0;
    border: 2px solid transparent;
    border-radius: var(--radius-sm);
    overflow: hidden;
    cursor: pointer;
    background: none;
    aspect-ratio: 1;
}
.photo-picker-item:hover { border-color: var(--primary); }
.photo-picker-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
