/*! Designframework 1.2.0 — 2026-09-19
 * https://static.jsengenberger.de/v1/framework.css
 * Reihenfolge ist tragend: Token, Basis, Komponenten.
 */
/**
 * Design Tokens
 *
 * Single source of truth for every colour, size and timing in the framework.
 * Nothing below this layer may contain a literal colour or a magic number.
 *
 * Origin: derived from the RSD-Vote framework (vote.jsengenberger.de),
 * `src/public/assets/css/framework/core/_variables.css`. Values were copied,
 * not re-derived — the two products are meant to look like siblings.
 *
 * Themes: light is the default. Dark is selected either explicitly through
 * `[data-theme="dark"]` on the root element, or implicitly by the operating
 * system when no explicit choice was made.
 */

:root {
    /* ---------------------------------------------------------------
       Brand
       --------------------------------------------------------------- */
    --color-primary: #e75421;
    --color-primary-light: #f16a3e;
    --color-primary-dark: #c64419;
    --color-primary-rgb: 231, 84, 33;
    --color-primary-alpha: rgba(231, 84, 33, 0.1);

    /* Text on a filled primary surface. Measures 3.69:1 — the one deviation
       the framework accepts rather than fixes, see REGELN.md §5. */
    --color-primary-ink: #ffffff;

    /* Primary used AS TEXT (links, active navigation, tinted badges). The
       brand fill is too light to carry small text: #e75421 reaches only
       3.69:1 on white and 4.25:1 on its own tint. This value clears the
       worst case, the tinted badge and the active navigation pill. */
    --color-primary-text: #b83d16;

    /* ---------------------------------------------------------------
       Status
       One meaning per hue. Never reuse a status colour decoratively.
       --------------------------------------------------------------- */
    --color-success: #66bb6a;
    --color-success-light: #81c784;
    --color-success-dark: #4caf50;
    --color-success-alpha: rgba(102, 187, 106, 0.1);

    --color-warning: #ff9800;
    --color-warning-light: #ffb74d;
    --color-warning-dark: #f57c00;
    --color-warning-alpha: rgba(255, 152, 0, 0.1);

    --color-error: #ef5350;
    --color-error-light: #e57373;
    --color-error-dark: #e53935;
    --color-error-alpha: rgba(239, 83, 80, 0.1);

    /* Schrift auf gefüllter Gefahrenfläche. Eigenes Token, weil --text-inverse im
       dunklen Thema auf #1a1a1a umschlägt und dunkle Schrift auf #ef5350 falsch wäre.
       Bewusst in beiden Themen gleich — deshalb steht es nur hier. */
    --color-error-ink: #ffffff;

    --color-info: #2196f3;
    --color-info-light: #64b5f6;
    --color-info-dark: #1976d2;
    --color-info-alpha: rgba(33, 150, 243, 0.1);

    /* ---------------------------------------------------------------
       Surfaces — light theme
       --------------------------------------------------------------- */
    --bg-primary: #ffffff;      /* cards, navbar, controls */
    --bg-secondary: #f8f9fa;    /* page ground, card footers, table heads */
    --bg-tertiary: #e9ecef;     /* icon tiles, code blocks, progress track */

    --text-primary: #2c3e50;
    /* Origin value was #6c757d. It misses AA on the raised surfaces this
       framework actually uses it on — 4.45:1 on --bg-secondary, 4.29:1 on
       --bg-tertiary. Darkened until it clears the worst case. */
    --text-secondary: #616970;
    --text-tertiary: #808080;
    --text-muted: #adb5bd;
    --text-inverse: #ffffff;

    --border-color: #e0e0e0;
    --border-color-light: #f0f0f0;
    --border-color-dark: #d0d0d0;

    --hover-bg: #f8f9fa;
    --active-bg: #e9ecef;

    --color-backdrop: rgba(0, 0, 0, 0.5);

    /* ---------------------------------------------------------------
       Map — the ground of src/v1/map (ENTWURF-KARTE.md §5).
       Muted on purpose: the map is ground, not signal. Water is a grey
       blue, not --color-info; green is not --color-success — one tone,
       one role (REGELN.md §3). Labels and highlights take the text and
       status-text tokens and are measured against --map-land.
       --------------------------------------------------------------- */
    --map-land: #f3f4f6;
    --map-water: #cbd8e1;
    --map-green: #e0eadd;
    --map-building: #e4e6e9;
    --map-road: #ffffff;

    /* Categories: six hues for maps that tell things apart rather than
       rate them (corridors, lines, districts). Only on the map, only with a
       legend, never as a status (REGELN.md §3). Chosen away from the brand
       (15°) and the four status hues (0°, 36°, 123°, 213°): violet 271°,
       petrol 182°, magenta 324°, olive 69°, indigo 232°, and a warm
       graphite that stays apart from the blue-grey ink. Each is measured
       3 : 1 against --map-land in both themes. */
    --map-cat-1: #7b3fb5;
    --map-cat-2: #00787c;
    --map-cat-3: #b02d7c;
    --map-cat-4: #657315;
    --map-cat-5: #3f4fbf;
    --map-cat-6: #6f6259;

    /* ---------------------------------------------------------------
       Component tokens that must differ per theme
       Added by this framework — see REGELN.md §5 "Abweichungen".
       --------------------------------------------------------------- */

    /* Status used AS TEXT — in badges, alerts, form errors, stat tiles and
       code marks. The pure status hues reach only 2.0 to 3.1 : 1 on a white
       ground, and the framework's own -dark variants only 2.5 to 4.1 : 1.
       These four darkened values are the framework's single deliberate
       departure from the origin palette; every one is measured in
       werkzeuge/kontrast.mjs and documented in REGELN.md §5.
       The tinted SURFACES are unchanged — only the text on them. */
    --status-success-text: #2e7d32;
    --status-warning-text: #8a5300;
    --status-error-text: #c62828;
    --status-info-text: #0d5bb9;

    /* Chevron of .form-select, inlined because a stylesheet cannot recolour
       a background image. Overridden in the dark theme. */
    --select-arrow: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23616970' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");

    /* ---------------------------------------------------------------
       Elevation
       --------------------------------------------------------------- */
    --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
    --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.05);
    --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08), 0 4px 6px rgba(0, 0, 0, 0.05);
    --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04);

    /* ---------------------------------------------------------------
       Geometry
       --------------------------------------------------------------- */
    --radius-xs: 2px;
    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 12px;
    --radius-xl: 16px;
    --radius-full: 9999px;

    --spacing-2xs: 2px;
    --spacing-xs: 4px;
    --spacing-sm: 8px;
    --spacing-md: 16px;
    --spacing-lg: 24px;
    --spacing-xl: 32px;
    --spacing-2xl: 48px;
    --spacing-3xl: 64px;

    /* ---------------------------------------------------------------
       Typography
       System stacks only. No web font, ever — see REGELN.md §2.
       --------------------------------------------------------------- */
    --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
                   'Helvetica Neue', Arial, sans-serif;
    --font-family-mono: 'SF Mono', 'Fira Code', 'Consolas', ui-monospace, monospace;

    --font-size-2xs: 0.625rem;  /* 10 — nur Marken und Badges, nie Fließtext (REGELN.md §5) */
    --font-size-xs: 0.75rem;    /* 12 */
    --font-size-sm: 0.875rem;   /* 14 */
    --font-size-base: 1rem;     /* 16 */
    --font-size-lg: 1.125rem;   /* 18 */
    --font-size-xl: 1.25rem;    /* 20 */
    --font-size-2xl: 1.5rem;    /* 24 */
    --font-size-3xl: 1.875rem;  /* 30 */
    --font-size-4xl: 2.25rem;   /* 36 */

    --font-weight-normal: 400;
    --font-weight-medium: 500;
    --font-weight-semibold: 600;
    --font-weight-bold: 700;

    --line-height-tight: 1.25;
    --line-height-snug: 1.375;
    --line-height-normal: 1.5;
    --line-height-relaxed: 1.625;

    --letter-spacing-tight: -0.025em;
    --letter-spacing-normal: 0;
    --letter-spacing-wide: 0.025em;

    /* ---------------------------------------------------------------
       Controls and layout
       --------------------------------------------------------------- */
    --input-height-sm: 32px;
    --input-height: 40px;
    --input-height-lg: 48px;
    /* A field inside a table cell (.table--edit): wide enough for a name,
       narrow enough that six columns still fit a laptop; in ch so that it
       follows the field's own font size, also on touch. */
    --table-field-width: 14ch;

    --button-height-sm: 32px;
    --button-height: 40px;
    --button-height-lg: 48px;

    --navbar-height: 64px;
    --container-max-width: 1400px;
    --container-narrow: 720px;

    --focus-ring-color: rgba(231, 84, 33, 0.4);
    --focus-ring-width: 3px;
    --focus-ring-offset: 2px;

    --transition-fast: 150ms ease;
    --transition-normal: 250ms ease;
    --transition-slow: 400ms ease;

    --z-dropdown: 100;
    --z-sticky: 150;
    --z-modal-backdrop: 200;
    --z-modal: 250;
    --z-toast: 400;
}

/* ===================================================================
   Dark theme

   Only surfaces, text, borders, elevation, the map ground and the two
   component tokens change. The brand and status hues are deliberately identical in both
   themes — a status must not mean a different colour depending on the
   viewer's setting.
   =================================================================== */

/* One block, two selectors: an explicit choice, and the system preference
   whenever the user has not explicitly chosen light. */
[data-theme="dark"] {
    --bg-primary: #1a1a1a;
    --bg-secondary: #242424;
    --bg-tertiary: #2e2e2e;

    --text-primary: #e8e8e8;
    --text-secondary: #a0a0a0;
    --text-tertiary: #808080;
    --text-muted: #707070;
    --text-inverse: #1a1a1a;

    --border-color: #3a3a3a;
    --border-color-light: #2e2e2e;
    --border-color-dark: #4a4a4a;

    --hover-bg: #2e2e2e;
    --active-bg: #383838;

    --color-backdrop: rgba(0, 0, 0, 0.7);

    --map-land: #1f1f1f;
    --map-water: #25313a;
    --map-green: #222b22;
    --map-building: #2a2a2a;
    --map-road: #333333;
    --map-cat-1: #b78be8;
    --map-cat-2: #3cbdc0;
    --map-cat-3: #e27cb8;
    --map-cat-4: #a9bb4c;
    --map-cat-5: #8f9df2;
    --map-cat-6: #b5a99f;

    /* Base #e75421 reaches only 4.25:1 on its own tint; the lighter brand
       variant clears it at 5.13:1. */
    --color-primary-text: var(--color-primary-light);

    /* On a near-black ground the pure hues carry themselves — 6.24 / 6.82 /
       4.90 measured on the tinted surface. Only error falls 0.02 short at
       4.48, so it takes the lighter variant and reaches 5.22. */
    --status-success-text: var(--color-success);
    --status-warning-text: var(--color-warning);
    --status-error-text: var(--color-error-light);
    --status-info-text: var(--color-info);

    /* Same chevron, stroke follows --text-secondary of this theme. */
    --select-arrow: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23a0a0a0' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");

    --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2);
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.2);
    --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.25);
    --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.35), 0 4px 6px rgba(0, 0, 0, 0.25);
    --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.4), 0 10px 10px rgba(0, 0, 0, 0.2);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg-primary: #1a1a1a;
        --bg-secondary: #242424;
        --bg-tertiary: #2e2e2e;

        --text-primary: #e8e8e8;
        --text-secondary: #a0a0a0;
        --text-tertiary: #808080;
        --text-muted: #707070;
        --text-inverse: #1a1a1a;

        --border-color: #3a3a3a;
        --border-color-light: #2e2e2e;
        --border-color-dark: #4a4a4a;

        --hover-bg: #2e2e2e;
        --active-bg: #383838;

        --color-backdrop: rgba(0, 0, 0, 0.7);

        --map-land: #1f1f1f;
        --map-water: #25313a;
        --map-green: #222b22;
        --map-building: #2a2a2a;
        --map-road: #333333;
        --map-cat-1: #b78be8;
        --map-cat-2: #3cbdc0;
        --map-cat-3: #e27cb8;
        --map-cat-4: #a9bb4c;
        --map-cat-5: #8f9df2;
        --map-cat-6: #b5a99f;

        --color-primary-text: var(--color-primary-light);

        --status-success-text: var(--color-success);
        --status-warning-text: var(--color-warning);
        --status-error-text: var(--color-error-light);
        --status-info-text: var(--color-info);

        --select-arrow: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23a0a0a0' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");

        --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2);
        --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.2);
        --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.25);
        --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.35), 0 4px 6px rgba(0, 0, 0, 0.25);
        --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.4), 0 10px 10px rgba(0, 0, 0, 0.2);
    }
}

/**
 * Base
 *
 * Normalisation plus the default look of bare HTML elements. A page that
 * uses no class at all should already look like it belongs to the system.
 * Every value here comes from 01-tokens.css.
 */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
    scrollbar-color: var(--border-color-dark) var(--bg-secondary);
}

body {
    margin: 0;
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    font-weight: var(--font-weight-normal);
    line-height: var(--line-height-normal);
    color: var(--text-primary);
    background-color: var(--bg-secondary);
    -webkit-font-smoothing: antialiased;
}

/* The `hidden` attribute must win against any display a component sets.
   Without this a `.dropzone[hidden]` would still render, because the
   component's `display: flex` outranks the user-agent default. */
[hidden] { display: none !important; }

/* ---------------------------------------------------------------
   Headings
   --------------------------------------------------------------- */

h1, h2, h3, h4, h5, h6 {
    margin: 0 0 var(--spacing-sm);
    color: var(--text-primary);
    font-weight: var(--font-weight-semibold);
    line-height: var(--line-height-tight);
    letter-spacing: var(--letter-spacing-tight);
    text-wrap: balance;
}

h1 { font-size: var(--font-size-4xl); font-weight: var(--font-weight-bold); margin-bottom: var(--spacing-md); }
h2 { font-size: var(--font-size-3xl); margin-bottom: var(--spacing-md); }
h3 { font-size: var(--font-size-2xl); }
h4 { font-size: var(--font-size-xl); }
h5 { font-size: var(--font-size-lg); }

/* Section label: the small, spaced, upright heading used above a group. */
h6 {
    font-size: var(--font-size-base);
    text-transform: uppercase;
    letter-spacing: var(--letter-spacing-wide);
}

/* ---------------------------------------------------------------
   Text
   --------------------------------------------------------------- */

p {
    margin: 0 0 var(--spacing-md);
    text-wrap: pretty;
}

p:last-child { margin-bottom: 0; }

/* Hover is marked by the underline, not by a colour change — a state that
   only exists in colour is invisible to part of the audience. */
a {
    color: var(--color-primary-text);
    text-decoration: none;
    transition: color var(--transition-fast);
}

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

small { font-size: var(--font-size-sm); color: var(--text-secondary); }

strong, b { font-weight: var(--font-weight-semibold); }

hr {
    height: 1px;
    margin: var(--spacing-lg) 0;
    border: 0;
    background: var(--border-color);
}

ul, ol { margin: 0 0 var(--spacing-md); padding-left: var(--spacing-lg); }
li { margin-bottom: var(--spacing-xs); }

/* ---------------------------------------------------------------
   Code

   Foreign document values are shown here. Never render them as HTML —
   see REGELN.md §6.
   --------------------------------------------------------------- */

code {
    font-family: var(--font-family-mono);
    font-size: var(--font-size-sm);
    color: var(--text-primary);
    background-color: var(--bg-tertiary);
    padding: var(--spacing-2xs) var(--spacing-xs);
    border-radius: var(--radius-sm);
}

pre {
    margin: 0 0 var(--spacing-md);
    padding: var(--spacing-md);
    font-family: var(--font-family-mono);
    font-size: var(--font-size-sm);
    line-height: var(--line-height-relaxed);
    color: var(--text-primary);
    /* --bg-secondary, not --bg-tertiary: the status hues used to mark finds
       inside a block reach only 4.32 : 1 on the tertiary surface. The 1px
       border keeps the block distinct without the extra step. */
    background-color: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    overflow-x: auto;
}

pre code {
    padding: 0;
    background-color: transparent;
    color: inherit;
}

/* Emphasis inside a code block borrows the status hues so that colour keeps
   meaning the same thing everywhere. No separate syntax palette. */
.code-mark--high { color: var(--status-error-text); }
.code-mark--medium { color: var(--status-warning-text); }
.code-mark--low { color: var(--status-info-text); }

kbd {
    font-family: var(--font-family-mono);
    font-size: var(--font-size-xs);
    padding: var(--spacing-2xs) var(--spacing-xs);
    color: var(--text-primary);
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-bottom-width: 2px;
    border-radius: var(--radius-sm);
}

/* ---------------------------------------------------------------
   Media and tables
   --------------------------------------------------------------- */

img, svg, video { max-width: 100%; height: auto; }

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

/* ---------------------------------------------------------------
   Focus

   Keyboard focus is always visible. Removing the ring without an equally
   visible replacement is forbidden — see REGELN.md §4.
   --------------------------------------------------------------- */

:focus-visible {
    outline: var(--focus-ring-width) solid var(--focus-ring-color);
    outline-offset: var(--focus-ring-offset);
}

::selection {
    background-color: var(--color-primary-alpha);
    color: var(--text-primary);
}

/* ---------------------------------------------------------------
   Motion

   Every transition in this framework is decorative. Switch them off for
   anyone who asked the system to reduce motion.
   --------------------------------------------------------------- */

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

/**
 * Components
 *
 * The complete vocabulary. Anything a page needs should be assembled from
 * these classes; a new component belongs in this file, not in a page.
 *
 * Naming follows the origin framework: element classes are flat
 * (`.btn`, `.badge`), children use BEM (`.card__header`), variants use a
 * single dash (`.btn-primary`, `.badge--success`). Keeping the names means
 * markup can move between this project and vote.jsengenberger.de unchanged.
 */

/* ===================================================================
   Layout
   =================================================================== */

.container {
    width: 100%;
    max-width: var(--container-max-width);
    margin: 0 auto;
    padding: 0 var(--spacing-md);
}

.container--narrow { max-width: var(--container-narrow); }

/* Top and bottom only. The shorthand used to set the sides to 0 as well and
   silently cancelled the side padding of .container on the same element —
   content sat flush against the screen edge at every width below 1400 px. */
.page {
    padding-top: var(--spacing-xl);
    padding-bottom: var(--spacing-3xl);
}

.page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--spacing-md);
    margin-bottom: var(--spacing-xl);
    /* Scrolled into view by script or anchor, the header must land below the
       sticky navbar, not under it. */
    scroll-margin-top: calc(var(--navbar-height) + var(--spacing-md));
}

.page-header__title { margin: 0; }

.page-header__subtitle {
    margin: var(--spacing-xs) 0 0;
    font-size: var(--font-size-base);
    color: var(--text-secondary);
}

.back-link {
    display: inline-block;
    margin-bottom: var(--spacing-md);
    font-size: var(--font-size-sm);
    color: var(--text-secondary);
}

.back-link:hover { color: var(--text-primary); text-decoration: none; }

/* Vertical rhythm helper. Prefer this over margins on the children. */
.stack { display: flex; flex-direction: column; gap: var(--spacing-lg); }
.stack--sm { gap: var(--spacing-sm); }
.stack--md { gap: var(--spacing-md); }

.row { display: flex; flex-wrap: wrap; gap: var(--spacing-sm); align-items: center; }

/* The actions that apply to the whole page — counter, discard, download.
   Scrolled to by script it lands below the sticky navbar; on a phone the
   primary action takes a full line of its own, last, where the thumb is. */
.row--actions { scroll-margin-top: calc(var(--navbar-height) + var(--spacing-md)); }

.grid { display: grid; gap: var(--spacing-md); }
.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

@media (max-width: 768px) {
    .grid--2, .grid--3 { grid-template-columns: minmax(0, 1fr); }
    /* Four tiles of one number each are a 2 × 2 block on a phone, not a
       column of four (REGELN.md §9). */
    .grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .page-header { flex-direction: column; }
    /* A file name at 36 px fills a phone screen with two words. */
    .page-header__title { font-size: var(--font-size-2xl); }
    .row--actions .btn-primary { flex-basis: 100%; }
}

/* ===================================================================
   Navbar
   =================================================================== */

.navbar {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    height: var(--navbar-height);
    background-color: var(--bg-primary);
    border-bottom: 1px solid var(--border-color);
}

.navbar__container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-md);
    height: 100%;
    max-width: var(--container-max-width);
    margin: 0 auto;
    padding: 0 var(--spacing-md);
}

.navbar__brand {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--text-primary);
    text-decoration: none;
}

.navbar__brand:hover { color: var(--text-primary); text-decoration: none; }

/* Square brand tile carrying a single letter. */
.navbar__mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    background: var(--color-primary);
    color: var(--color-primary-ink);
    border-radius: var(--radius-md);
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-bold);
}

.navbar__menu { display: flex; align-items: center; gap: var(--spacing-xs); }

.navbar__link {
    display: flex;
    align-items: center;
    gap: var(--spacing-xs);
    padding: var(--spacing-sm) var(--spacing-md);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--text-secondary);
    border-radius: var(--radius-md);
    text-decoration: none;
    transition: color var(--transition-fast), background-color var(--transition-fast);
}

.navbar__link:hover { color: var(--text-primary); background-color: var(--hover-bg); text-decoration: none; }
.navbar__link--active { color: var(--color-primary-text); background-color: var(--color-primary-alpha); }

.navbar__user {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    padding: var(--spacing-xs) var(--spacing-md) var(--spacing-xs) var(--spacing-xs);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-full);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
}

.navbar__avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    background: var(--color-primary);
    color: var(--color-primary-ink);
    border-radius: var(--radius-full);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
}

@media (max-width: 768px) {
    .navbar__menu { display: none; }
}

/* ===================================================================
   Tabs

   One page, several views. The bar sticks under the navbar so that a long
   panel never puts the other views out of reach — on a phone that is the
   whole point. The active tab is marked by the underline and the brand
   text together (REGELN.md §3); when the labels do not fit, the bar scrolls
   sideways in its own box (§9). Behaviour — roles, arrow keys, which panel
   shows — is the page's job, as with the dialog's focus trap.
   =================================================================== */

.tabs {
    position: sticky;
    top: var(--navbar-height);
    /* Below the navbar, above everything that scrolls under it. */
    z-index: calc(var(--z-sticky) - 1);
    display: flex;
    gap: var(--spacing-xs);
    background: var(--bg-secondary);
    /* The rule is an inset shadow, not a border: a child's underline paints
       over it, and nothing hangs out of the scroll box. */
    box-shadow: inset 0 -1px 0 var(--border-color);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}

.tabs__tab {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    gap: var(--spacing-sm);
    height: var(--button-height-lg);
    padding: 0 var(--spacing-md);
    font-family: inherit;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    line-height: 1;
    white-space: nowrap;
    color: var(--text-secondary);
    background: transparent;
    border: 0;
    border-bottom: 2px solid transparent;
    cursor: pointer;
    transition: color var(--transition-fast), border-color var(--transition-fast);
}

.tabs__tab:hover { color: var(--text-primary); border-bottom-color: var(--border-color-dark); }
.tabs__tab--active,
.tabs__tab--active:hover { color: var(--color-primary-text); border-bottom-color: var(--color-primary); }
.tabs__tab--active .badge { background: var(--color-primary-alpha); color: var(--color-primary-text); }
/* The ring stays inside the scroll box; outside it would be clipped. */
.tabs__tab:focus-visible { outline-offset: calc(-1 * var(--focus-ring-width)); }

/* Four tabs on a phone are wider than the bar (measured: 501 px on 358 px,
   the first three filling it exactly). Tighter padding does not make them
   fit, but it lets the last tab show its edge instead of hiding behind the
   scroll box — the cut-off tab is the cue that the bar scrolls. */
@media (max-width: 768px) {
    .tabs__tab { padding: 0 var(--spacing-sm); }
}

/* A panel scrolled to by script lands below navbar and bar, not under them. */
.tab-panel { scroll-margin-top: calc(var(--navbar-height) + var(--button-height-lg) + var(--spacing-md)); }

/* ===================================================================
   Buttons

   Height is fixed, line-height is 1, padding is horizontal only — that is
   what keeps a row of buttons aligned regardless of label length.
   =================================================================== */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-xs);
    height: var(--button-height);
    padding: 0 var(--spacing-lg);
    font-family: inherit;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    line-height: 1;
    white-space: nowrap;
    text-decoration: none;
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: all var(--transition-fast);
}

.btn:hover { text-decoration: none; }
.btn:active:not(:disabled) { transform: translateY(1px); }

.btn:disabled,
.btn.is-disabled {
    opacity: 0.6;
    cursor: not-allowed;
    pointer-events: none;
}

.btn-sm { height: var(--button-height-sm); padding: 0 var(--spacing-md); font-size: var(--font-size-xs); border-radius: var(--radius-sm); }
/* Under a thumb a small button is a full-height button. Bound to the
   pointer, not to the width: an iPad is wide and is still touched. */
@media (pointer: coarse) {
    .btn-sm { height: var(--button-height); }
}
.btn-lg { height: var(--button-height-lg); padding: 0 var(--spacing-xl); font-size: var(--font-size-base); }
.btn-block { display: flex; width: 100%; }

.btn-primary { background: var(--color-primary); color: var(--color-primary-ink); border-color: var(--color-primary); }
.btn-primary:hover:not(:disabled) { background: var(--color-primary-dark); border-color: var(--color-primary-dark); color: var(--color-primary-ink); }

.btn-secondary { background: var(--bg-primary); color: var(--text-primary); border-color: var(--border-color); }
.btn-secondary:hover:not(:disabled) { background: var(--hover-bg); border-color: var(--border-color-dark); color: var(--text-primary); }

.btn-success { background: var(--color-success); color: var(--text-inverse); border-color: var(--color-success); }
.btn-success:hover:not(:disabled) { background: var(--color-success-dark); border-color: var(--color-success-dark); }

.btn-warning { background: var(--color-warning); color: var(--text-inverse); border-color: var(--color-warning); }
.btn-warning:hover:not(:disabled) { background: var(--color-warning-dark); border-color: var(--color-warning-dark); }

.btn-danger { background: var(--color-error); color: var(--color-error-ink); border-color: var(--color-error); }
.btn-danger:hover:not(:disabled) { background: var(--color-error-dark); border-color: var(--color-error-dark); }

.btn-ghost { background: transparent; color: var(--text-secondary); border-color: transparent; }
.btn-ghost:hover:not(:disabled) { background: var(--hover-bg); color: var(--text-primary); }

/* ===================================================================
   Badges
   =================================================================== */

.badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-xs);
    padding: var(--spacing-2xs) var(--spacing-sm);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-medium);
    line-height: var(--line-height-normal);
    white-space: nowrap;
    border-radius: var(--radius-full);
    background: var(--bg-tertiary);
    color: var(--text-secondary);
}

.badge--sm { padding: var(--spacing-2xs) var(--spacing-sm); font-size: var(--font-size-2xs); }
.badge--lg { padding: var(--spacing-xs) var(--spacing-md); font-size: var(--font-size-sm); }

.badge--primary { background: var(--color-primary-alpha); color: var(--color-primary-text); }
.badge--success { background: var(--color-success-alpha); color: var(--status-success-text); }
.badge--warning { background: var(--color-warning-alpha); color: var(--status-warning-text); }
.badge--danger  { background: var(--color-error-alpha);   color: var(--status-error-text); }
.badge--info    { background: var(--color-info-alpha);    color: var(--status-info-text); }

/* Leading dot. Colour alone never carries meaning, so a badge always has a
   label as well — the dot only makes the level scannable. */
.badge__dot {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-full);
    background: currentColor;
    flex-shrink: 0;
}

/* ===================================================================
   Cards
   =================================================================== */

/* A card answers to its own width, not to the viewport's: one of two
   columns on a tablet is as narrow as a phone, while a full-width card on
   that same tablet is not. Footer and key/value tables query the card
   (`@container`, REGELN.md §9). */
.card {
    container-type: inline-size;
    display: flex;
    flex-direction: column;
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.card__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-md);
    padding: var(--spacing-md) var(--spacing-lg);
    border-bottom: 1px solid var(--border-color);
}

.card__header-title {
    margin: 0;
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--text-primary);
}

.card__body { flex: 1; padding: var(--spacing-lg); }
.card__body--flush { padding: 0; }

.card__footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--spacing-sm);
    padding: var(--spacing-md) var(--spacing-lg);
    border-top: 1px solid var(--border-color);
    background: var(--bg-secondary);
}

.card__footer--between { justify-content: space-between; }
/* The note yields, the buttons do not: a long note wraps its lines instead of
   pushing the second button onto a line of its own, where it would sit left. */
.card__footer--between > .row { flex-shrink: 0; }
.card__footer--between > :not(.row) { min-width: 0; }

/* In a narrow card — a phone, or one of two columns on a tablet — the
   footer stacks: the note first, then the buttons, which share the width
   between them; a single button takes it all. */
@container (max-width: 560px) {
    .card__footer--between { flex-direction: column; align-items: stretch; }
    /* Two short buttons share a line; a long one takes a line of its own — never the edge. */
    .card__footer--between > .row { flex-wrap: wrap; }
    .card__footer--between > .row > .btn { flex: 1 1 auto; }
}

/* A single number with a tinted tile. The tile carries a letter or a glyph,
   never a decorative icon. */
.stat-card {
    display: flex;
    align-items: center;
    gap: var(--spacing-md);
    padding: var(--spacing-lg);
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
}

.stat-card__tile {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-md);
    background: var(--bg-tertiary);
    color: var(--text-secondary);
    font-weight: var(--font-weight-bold);
}

/* A meaningful tile carries the tint of its own status, not the neutral one:
   a status hue on --bg-tertiary sits at 3.7 to 4.4 : 1, on its own tint it
   clears AA. The tint also makes the tile itself carry the meaning. */
.stat-card__tile--primary { background: var(--color-primary-alpha); color: var(--color-primary-text); }
.stat-card__tile--success { background: var(--color-success-alpha); color: var(--status-success-text); }
.stat-card__tile--warning { background: var(--color-warning-alpha); color: var(--status-warning-text); }
.stat-card__tile--danger  { background: var(--color-error-alpha);   color: var(--status-error-text); }
.stat-card__tile--info    { background: var(--color-info-alpha);    color: var(--status-info-text); }

.stat-card__value {
    font-size: var(--font-size-3xl);
    font-weight: var(--font-weight-bold);
    line-height: var(--line-height-tight);
    letter-spacing: var(--letter-spacing-tight);
    font-variant-numeric: tabular-nums;
}

.stat-card__label { font-size: var(--font-size-sm); color: var(--text-secondary); }

/* A tile that leads somewhere is a link. It keeps the text colour of a tile
   and marks hover by underlining the label, not by colour (REGELN.md §3). */
a.stat-card { color: inherit; }
a.stat-card:hover { border-color: var(--border-color-dark); text-decoration: none; }
a.stat-card:hover .stat-card__label { text-decoration: underline; }

/* Two tiles share a phone's width, so each gives up some air. */
@media (max-width: 768px) {
    .stat-card { gap: var(--spacing-sm); padding: var(--spacing-md); }
}

/* A row inside a card body: recessed, not bordered. */
.list-item {
    display: flex;
    align-items: flex-start;
    gap: var(--spacing-md);
    padding: var(--spacing-md);
    background: var(--bg-secondary);
    border-radius: var(--radius-md);
}

/* A reply in a thread: indented under the row it answers. */
.list-item--reply { margin-left: var(--spacing-xl); }
@media (max-width: 768px) {
    .list-item--reply { margin-left: var(--spacing-md); }
}

/* The chosen row. The tint follows .navbar__link--active; the choice itself
   is carried by a control inside the row, never by the colour alone. */
.list-item.is-selected { background: var(--color-primary-alpha); }

/* A row whose right end carries actions. The label — control and content —
   takes the width, the actions keep theirs; buttons cannot live inside a
   label, so the label ends where the actions begin. In a narrow card the
   actions drop below the content, right-aligned, so the text is not squeezed. */
.list-item__label {
    display: flex;
    flex: 1 1 auto;
    align-items: flex-start;
    gap: var(--spacing-md);
    min-width: 0;
    cursor: pointer;
}
.list-item__actions { flex-shrink: 0; margin-left: auto; }
@container (max-width: 560px) {
    .list-item { flex-wrap: wrap; }
    .list-item__actions { flex-basis: 100%; justify-content: flex-end; }
}

/* The passage a comment or a finding refers to: quoted, set off by a rule,
   never louder than the text that talks about it. */
.quote {
    margin: 0;
    padding-left: var(--spacing-sm);
    border-left: 2px solid var(--border-color-dark);
    font-size: var(--font-size-sm);
    color: var(--text-secondary);
}

/* ===================================================================
   Forms
   =================================================================== */

.form-group { margin-bottom: var(--spacing-md); }
.form-group:last-child { margin-bottom: 0; }

.form-label {
    display: block;
    margin-bottom: var(--spacing-xs);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
}

.form-label--required::after { content: " *"; color: var(--color-error); }

.form-control {
    display: block;
    width: 100%;
    height: var(--input-height);
    padding: var(--spacing-sm) var(--spacing-md);
    font-family: inherit;
    font-size: var(--font-size-base);
    line-height: var(--line-height-normal);
    color: var(--text-primary);
    background-color: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

/* Placeholders carry meaning, so they follow --text-secondary. --text-muted
   is reserved for disabled controls, which WCAG exempts. */
.form-control::placeholder { color: var(--text-secondary); opacity: 1; }
.form-control:hover:not(:disabled):not(:focus) { border-color: var(--border-color-dark); }

.form-control:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
}

.form-control:disabled {
    background-color: var(--bg-tertiary);
    color: var(--text-muted);
    cursor: not-allowed;
    opacity: 0.7;
}

.form-control--sm { height: var(--input-height-sm); padding: var(--spacing-xs) var(--spacing-sm); font-size: var(--font-size-sm); border-radius: var(--radius-sm); }
.form-control--lg { height: var(--input-height-lg); padding: var(--spacing-md) var(--spacing-lg); font-size: var(--font-size-lg); }

.form-control.is-invalid { border-color: var(--color-error); }
.form-control.is-invalid:focus { border-color: var(--color-error); box-shadow: 0 0 0 var(--focus-ring-width) var(--color-error-alpha); }

/* Values read out of a foreign file are monospace, so that a trailing space
   or a lookalike character stays visible. */
.form-control--mono { font-family: var(--font-family-mono); font-size: var(--font-size-sm); }

/* On a touch device no field is smaller than the base size: iOS zooms into
   any field below 16 px, and a thumb needs the full height. */
@media (pointer: coarse) {
    .form-control--sm { height: var(--input-height); padding: var(--spacing-sm) var(--spacing-md); font-size: var(--font-size-base); }
    .form-control--mono { font-size: var(--font-size-base); }
    .form-textarea--grow { height: auto; min-height: var(--input-height); }
}

.form-select {
    padding-right: var(--spacing-xl);
    background-image: var(--select-arrow);
    background-repeat: no-repeat;
    background-position: right var(--spacing-md) center;
    background-size: 16px 12px;
    appearance: none;
    cursor: pointer;
}

.form-textarea {
    height: auto;
    min-height: 100px;
    resize: vertical;
}

/* A textarea that begins as one line and grows with what is typed — for a
   value that is usually one line and sometimes an address. The page sets
   `rows` as the user types; there is no handle and no scrollbar, the field
   is simply as tall as its lines. Declared after --sm so that it wins the
   fixed height, and again under coarse pointers for the same reason. */
.form-textarea--grow {
    height: auto;
    min-height: var(--input-height-sm);
    resize: none;
    overflow: hidden;
}

/* A control and the buttons that belong to it, on one line: the control
   takes the width, the buttons keep theirs. Never wraps — a "clear" button
   under its field reads as a separate action. */
.input-group {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
}
.input-group .form-control { flex: 1; min-width: 0; }

.form-hint { display: block; margin-top: var(--spacing-xs); font-size: var(--font-size-xs); color: var(--text-secondary); }
.form-error { display: block; margin-top: var(--spacing-xs); font-size: var(--font-size-xs); color: var(--status-error-text); }

.form-check { display: flex; align-items: flex-start; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); }
.form-check:last-child { margin-bottom: 0; }

.form-check-input {
    flex-shrink: 0;
    width: 18px;
    height: 18px;
    margin: var(--spacing-2xs) 0 0;
    background-color: var(--bg-primary);
    background-repeat: no-repeat;
    background-position: center;
    border: 2px solid var(--border-color);
    appearance: none;
    cursor: pointer;
    transition: all var(--transition-fast);
}

.form-check-input[type="checkbox"] { border-radius: var(--radius-sm); }
.form-check-input[type="radio"] { border-radius: var(--radius-full); }
.form-check-input:hover { border-color: var(--color-primary); }

.form-check-input:checked {
    background-color: var(--color-primary);
    border-color: var(--color-primary);
}

.form-check-input[type="checkbox"]:checked {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='white' d='M13.485 3.485a.75.75 0 0 1 0 1.06l-6.5 6.5a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 0 1 1.06-1.06l2.47 2.47 5.97-5.97a.75.75 0 0 1 1.06 0z'/%3e%3c/svg%3e");
    background-size: 12px;
}

.form-check-input[type="radio"]:checked {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3ccircle cx='8' cy='8' r='4' fill='white'/%3e%3c/svg%3e");
}

.form-check-input:disabled { opacity: 0.5; cursor: not-allowed; }
.form-check-label { font-size: var(--font-size-base); color: var(--text-primary); cursor: pointer; }

/* Ein Häkchen wird über seine ganze Zeile getippt, nicht über das Kästchen: Die
   Beschriftung gehört per `for` zum Feld und ist Teil des Ziels. Deshalb trägt
   die Zeile das Tippziel von 40 px (REGELN.md §9); das Kästchen wächst nur so
   weit, dass ein Daumen auch es allein trifft. Ein 40 px hohes Kästchen neben
   einer Textzeile wäre ein Klotz. */
@media (pointer: coarse) {
    .form-check { min-height: var(--input-height); align-items: center; }
    .form-check-input { width: 24px; height: 24px; margin-top: 0; }
}

.form-switch { display: flex; align-items: center; gap: var(--spacing-sm); }

.form-switch-input {
    position: relative;
    width: 44px;
    height: 24px;
    background-color: var(--border-color);
    border: none;
    border-radius: var(--radius-full);
    appearance: none;
    cursor: pointer;
    transition: background-color var(--transition-fast);
}

.form-switch-input::before {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    background-color: var(--bg-primary);
    border-radius: var(--radius-full);
    box-shadow: var(--shadow-sm);
    transition: transform var(--transition-fast);
}

.form-switch-input:checked { background-color: var(--color-primary); }
.form-switch-input:checked::before { transform: translateX(20px); }
.form-switch-input:disabled { opacity: 0.5; cursor: not-allowed; }

/* Area that accepts a file. The only dashed border in the framework — it
   marks "something goes here", nothing else may borrow the idiom. */
.dropzone {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-2xl);
    text-align: center;
    background: var(--bg-primary);
    border: 1px dashed var(--border-color-dark);
    border-radius: var(--radius-lg);
    cursor: pointer;
    transition: border-color var(--transition-fast), background-color var(--transition-fast);
}

.dropzone:hover,
.dropzone.is-active {
    border-color: var(--color-primary);
    background: var(--color-primary-alpha);
}

/* ===================================================================
   Table
   =================================================================== */

.table { font-size: var(--font-size-sm); color: var(--text-primary); }

.table th,
.table td {
    padding: var(--spacing-sm) var(--spacing-md);
    text-align: left;
    vertical-align: middle;
    border-bottom: 1px solid var(--border-color);
}

.table th {
    font-weight: var(--font-weight-semibold);
    color: var(--text-secondary);
    background: var(--bg-secondary);
    white-space: nowrap;
}

.table tbody tr:last-child td,
.table tbody tr:last-child th { border-bottom: none; }

.table tbody tr:hover td,
.table tbody tr:hover th { background: var(--hover-bg); }

/* Key column of a key/value table: keeps the label on one line and lets
   the value column take the rest of the width. Written as `th scope="row"`
   for assistive technology, but styled as a label, not as a header cell —
   hence the transparent ground and the higher specificity than `.table th`. */
.table .table__key {
    width: 1%;
    white-space: nowrap;
    vertical-align: top;
    font-weight: var(--font-weight-medium);
    color: var(--text-secondary);
    background: transparent;
}
.table__mono { font-family: var(--font-family-mono); }
/* Doubled like .table__key: `.table th, .table td` (0,1,1) would otherwise win. */
.table .table__num { text-align: right; font-variant-numeric: tabular-nums; }
.table .table__actions { width: 1%; white-space: nowrap; text-align: right; }

/* A table wider than its column scrolls inside its own box, never the page.
   The box is positioned so that an absolutely positioned descendant — a
   .visually-hidden header label — stays inside it; otherwise it would sit
   at its static position in the page and widen the page instead. */
.table-scroll { overflow-x: auto; position: relative; }

/* Key/value tables in a narrow card (a phone, or one of two columns on a
   tablet — REGELN.md §9). Split 50/50 there, the value column is too narrow
   to read and an input in it too narrow to type in. `.table--kv` lets each
   row flow: a short value stays beside its key, a long one drops below it
   at full width. `.table--stacked` always puts the key above — for rows
   that hold an input, which wants the whole width. The cells stay th/td for
   assistive technology; only the boxes change. */
@container (max-width: 560px) {
    .table--kv, .table--kv tbody, .table--kv tr, .table--kv th, .table--kv td,
    .table--stacked, .table--stacked tbody, .table--stacked tr, .table--stacked th, .table--stacked td { display: block; }
    .table--kv tr, .table--stacked tr { padding: var(--spacing-xs) var(--spacing-md); border-bottom: 1px solid var(--border-color); }
    .table--kv tr:last-child, .table--stacked tr:last-child { border-bottom: 0; }
    .table--kv th, .table--kv td, .table--stacked th, .table--stacked td { padding: var(--spacing-2xs) 0; border-bottom: 0; }
    .table--kv .table__key, .table--stacked .table__key { width: auto; white-space: normal; font-size: var(--font-size-xs); }
    .table--kv tbody tr:hover th, .table--kv tbody tr:hover td,
    .table--stacked tbody tr:hover th, .table--stacked tbody tr:hover td { background: transparent; }
    .table--kv tbody tr:hover, .table--stacked tbody tr:hover { background: var(--hover-bg); }

    .table--kv tr { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--spacing-md); }
    .table--kv .table__key { flex: 0 1 auto; }
    .table--kv td { flex: 1 1 60%; min-width: 0; }
}

/* A wide preview — many columns, one row per record — keeps every cell on
   one line and scrolls in its .table-scroll box instead of breaking words.
   (Until 2026-09-04 this rule sat inside the container query above by
   mistake and only held in cards narrower than 560 px.) */
.table--nowrap th,
.table--nowrap td { white-space: nowrap; }

/* A table whose cells hold fields — one row per record to type in. A
   percentage-wide field in an auto-layout cell shrinks to nothing, so the
   field gets a width of its own and the table scrolls in its box instead
   (docx: the rows of a series typed by hand). */
.table--edit td > .form-control { width: var(--table-field-width); }

/* ===================================================================
   Alerts
   =================================================================== */

.alert {
    display: flex;
    align-items: flex-start;
    gap: var(--spacing-sm);
    padding: var(--spacing-md);
    font-size: var(--font-size-sm);
    line-height: var(--line-height-normal);
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
}

.alert__mark { flex-shrink: 0; font-size: var(--font-size-lg); font-weight: var(--font-weight-bold); line-height: 1.2; }
.alert__content { flex: 1; min-width: 0; }
.alert__title { font-weight: var(--font-weight-semibold); margin-bottom: var(--spacing-xs); }
.alert__message { margin: 0; }

.alert--success { background: var(--color-success-alpha); border-color: var(--color-success); color: var(--status-success-text); }
.alert--warning { background: var(--color-warning-alpha); border-color: var(--color-warning); color: var(--status-warning-text); }
.alert--danger  { background: var(--color-error-alpha);   border-color: var(--color-error);   color: var(--status-error-text); }
.alert--info    { background: var(--color-info-alpha);    border-color: var(--color-info);    color: var(--status-info-text); }

/* ===================================================================
   Progress
   =================================================================== */

.progress {
    display: flex;
    height: 8px;
    background: var(--bg-tertiary);
    border-radius: var(--radius-full);
    overflow: hidden;
}

.progress--sm { height: 4px; }
.progress--lg { height: 16px; }

.progress__fill {
    height: 100%;
    background: var(--color-primary);
    border-radius: var(--radius-full);
    transition: width var(--transition-slow);
}

.progress__fill--success { background: var(--color-success); }
.progress__fill--warning { background: var(--color-warning); }
.progress__fill--danger  { background: var(--color-error); }

/* ===================================================================
   Empty state
   =================================================================== */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-2xl);
    text-align: center;
}

.empty-state__mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 80px;
    height: 80px;
    margin-bottom: var(--spacing-lg);
    border-radius: var(--radius-full);
    background: var(--bg-tertiary);
    color: var(--text-muted);
    font-size: var(--font-size-3xl);
}

.empty-state__title { margin: 0 0 var(--spacing-sm); font-size: var(--font-size-xl); font-weight: var(--font-weight-semibold); }

.empty-state__text {
    margin: 0 0 var(--spacing-lg);
    max-width: 400px;
    font-size: var(--font-size-base);
    line-height: var(--line-height-relaxed);
    color: var(--text-secondary);
}

/* ===================================================================
   Modal
   =================================================================== */

.modal-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal-backdrop);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-lg);
    background: var(--color-backdrop);
    backdrop-filter: blur(2px);
}

.modal {
    position: relative;
    z-index: var(--z-modal);
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 500px;
    max-height: calc(100vh - var(--spacing-2xl));
    background: var(--bg-primary);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xl);
    overflow: hidden;
}

.modal--sm { max-width: 360px; }
.modal--lg { max-width: 700px; }

/* The same box as a native <dialog>: the browser brings the focus trap, Esc
   and an inert page behind it; the framework brings the look. The UA sheet
   positions a modal dialog itself and hides a closed one — both are author
   rules here, because `.modal` would otherwise pull it back into the flow
   and `display: flex` would show it closed. No .modal-overlay needed. */
dialog.modal {
    position: fixed;
    inset: 0;
    width: calc(100% - 2 * var(--spacing-lg));
    margin: auto;
    padding: 0;
    border: 0;
    color: var(--text-primary);
}
dialog.modal:not([open]) { display: none; }
dialog.modal::backdrop {
    background: var(--color-backdrop);
    backdrop-filter: blur(2px);
}

.modal__header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--spacing-md);
    padding: var(--spacing-lg);
    border-bottom: 1px solid var(--border-color);
    flex-shrink: 0;
}

.modal__title { margin: 0; font-size: var(--font-size-lg); font-weight: var(--font-weight-semibold); }
.modal__subtitle { margin: var(--spacing-xs) 0 0; font-size: var(--font-size-sm); color: var(--text-secondary); }
.modal__body { flex: 1; padding: var(--spacing-lg); overflow-y: auto; overscroll-behavior: contain; }

.modal__footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--spacing-sm);
    padding: var(--spacing-md) var(--spacing-lg);
    border-top: 1px solid var(--border-color);
    background: var(--bg-secondary);
    flex-shrink: 0;
}

/* ===================================================================
   Map

   The frame and furniture of src/v1/map/map.js (KOMPONENTEN.md "Landkarte").
   MapLibre's own stylesheet is loaded by map.js before this file, so these
   rules win ties; they restyle only what shows text or takes focus, so that
   it follows the theme. The map's colours themselves are --map-* tokens,
   read by the script.
   =================================================================== */

.map {
    position: relative;
    width: 100%;
    aspect-ratio: 3 / 2;
    min-height: 240px;
    overflow: hidden;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    background: var(--map-land);
}
@media (max-width: 768px) {
    .map { aspect-ratio: 1 / 1; }
}
.map:fullscreen { aspect-ratio: auto; border: 0; border-radius: 0; }
.map.maplibregl-map {
    font-family: var(--font-family);
    font-size: var(--font-size-sm);
    line-height: var(--line-height-normal);
}
/* The canvas fills the frame; an outer ring would be cut off by overflow. */
.map .maplibregl-canvas:focus-visible { outline-offset: calc(var(--focus-ring-width) * -1); }

.map__message {
    position: absolute;
    top: var(--spacing-sm);
    left: var(--spacing-sm);
    right: var(--spacing-sm);
    z-index: 3;                  /* above MapLibre's control corners (2) */
    margin: 0;
    padding: var(--spacing-sm) var(--spacing-md);
    background: var(--bg-primary);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
}

/* The colour of a mark: ink unless a class says otherwise — a status tone
   or one of the six categories (REGELN.md §3). Markers, pills and legend
   swatches all read --map-mark; lines get the same token from the script. */
.map-color--success { --map-mark: var(--status-success-text); }
.map-color--warning { --map-mark: var(--status-warning-text); }
.map-color--danger { --map-mark: var(--status-error-text); }
.map-color--info { --map-mark: var(--status-info-text); }
.map-color--cat-1 { --map-mark: var(--map-cat-1); }
.map-color--cat-2 { --map-mark: var(--map-cat-2); }
.map-color--cat-3 { --map-mark: var(--map-cat-3); }
.map-color--cat-4 { --map-mark: var(--map-cat-4); }
.map-color--cat-5 { --map-mark: var(--map-cat-5); }
.map-color--cat-6 { --map-mark: var(--map-cat-6); }

.map__marker {
    width: 18px;
    height: 18px;
    padding: 0;
    border: 3px solid var(--map-land);
    border-radius: 50%;
    background: var(--map-mark, var(--text-primary));
    box-shadow: var(--shadow-sm);
    cursor: pointer;
}
/* A marker that carries a short value ("+12 %", "Hbf"). The text sits on the
   mark's own colour in --map-land; every pair is measured ("Pille …" in
   tools/kontrast.mjs). */
.map__marker--pill {
    width: auto;
    height: auto;
    padding: var(--spacing-2xs) var(--spacing-sm);
    border-width: 2px;
    border-radius: var(--radius-full);
    color: var(--map-land);
    font-family: inherit;
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-semibold);
    line-height: var(--line-height-normal);
    white-space: nowrap;
}

.map__tools {
    display: flex;
    flex-direction: column;
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
}
.map__tool {
    display: grid;
    place-items: center;
    width: var(--button-height-sm);
    height: var(--button-height-sm);
    padding: 0;
    border: 0;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--text-primary);
    cursor: pointer;
}
.map__tool + .map__tool { border-top: 1px solid var(--border-color); border-top-left-radius: 0; border-top-right-radius: 0; }
.map__tool:first-child:not(:last-child) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.map__tool:hover { background: var(--hover-bg); }

.map__legend {
    max-width: 240px;
    background: var(--bg-primary);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    font-size: var(--font-size-xs);
    line-height: var(--line-height-snug);
}
.map__legend-title {
    display: flex;
    align-items: center;
    gap: var(--spacing-xs);
    padding: var(--spacing-xs) var(--spacing-sm);
    font-weight: var(--font-weight-semibold);
    list-style: none;
    cursor: pointer;
}
.map__legend-title::-webkit-details-marker { display: none; }
.map__legend-title svg { flex: none; color: var(--text-secondary); transition: transform var(--transition-fast); }
.map__legend details[open] > .map__legend-title svg { transform: rotate(90deg); }
.map__legend-list {
    display: grid;
    gap: var(--spacing-xs);
    margin: 0;
    padding: 0 var(--spacing-sm) var(--spacing-sm);
    list-style: none;
}
.map__legend-item { display: flex; align-items: center; gap: var(--spacing-sm); }
label.map__legend-item { cursor: pointer; }
.map__legend-item .form-check-input { margin: 0; }
.map__swatch { flex: none; background: var(--map-mark, var(--text-primary)); }
.map__swatch--line { width: 20px; height: 4px; border-radius: var(--radius-full); }
.map__swatch--marker { width: 12px; height: 12px; border-radius: 50%; }

.map .maplibregl-popup-content {
    padding: var(--spacing-sm) var(--spacing-2xl) var(--spacing-sm) var(--spacing-md);
    background: var(--bg-primary);
    color: var(--text-primary);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
}
.map__popup-title { display: block; font-weight: var(--font-weight-semibold); }
.map__popup-text { margin: var(--spacing-2xs) 0 0; color: var(--text-secondary); }
.map .maplibregl-popup-tip { display: none; }
.map .maplibregl-popup-close-button {
    width: var(--button-height-sm);
    height: var(--button-height-sm);
    color: var(--text-secondary);
    font-size: var(--font-size-lg);
    border-radius: var(--radius-md);
}
.map .maplibregl-popup-close-button:hover { background: var(--hover-bg); }

.map .maplibregl-ctrl-scale {
    padding: 0 var(--spacing-xs);
    background: var(--bg-primary);
    color: var(--text-secondary);
    border-color: var(--text-secondary);
    font-size: var(--font-size-xs);
}
.map .maplibregl-ctrl-attrib {
    background: var(--bg-primary);
    color: var(--text-secondary);
    font-size: var(--font-size-xs);
}
.map .maplibregl-ctrl-attrib a { color: inherit; text-decoration: underline; }

/* Touch: every target at least 40 px (REGELN.md §9); a legend row with a
   switch carries the 40 px, not the box. */
@media (pointer: coarse) {
    .map__marker:not(.map__marker--pill) { width: 24px; height: 24px; }
    .map__marker::after { content: ""; position: absolute; inset: -8px; }
    .map__tool,
    .map .maplibregl-popup-close-button { width: var(--button-height); height: var(--button-height); }
    .map__legend-title { padding: var(--spacing-sm); }
    .map__legend-item { min-height: var(--input-height); }
}

/* ===================================================================
   Utilities

   Deliberately few. A utility that starts describing a component belongs
   in this file as a component instead.
   =================================================================== */

.text-secondary { color: var(--text-secondary); }
.text-muted { color: var(--text-muted); }
.text-mono { font-family: var(--font-family-mono); }
.text-sm { font-size: var(--font-size-sm); }
.text-xs { font-size: var(--font-size-xs); }
/* Breaks at word boundaries first and only inside a word when a single
   token — a UNC path, a GUID — would otherwise overflow. `break-all` would
   also split ordinary prose mid-word. */
.text-break { overflow-wrap: anywhere; }

/* Words that only make sense with a mouse ("drag it here") or only without
   one. `pointer: fine` is a mouse, trackpad or pen; touch — or no pointing
   device at all — gets the other wording. */
@media (pointer: fine) { .touch-only { display: none !important; } }
@media not all and (pointer: fine) { .pointer-only { display: none !important; } }
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
