/*! Designframework 1.4.0 — 2026-11-01
 * 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 (--color-primary-fill). */
    --color-primary-ink: #ffffff;

    /* A filled brand surface that carries text — the main button, the
       avatar. White on the brand hue itself reaches only 3.69:1; one step
       darker it clears AA (REGELN.md §5). The brand hue stays for marks
       without small text: switch, check, progress, focus ring. */
    --color-primary-fill: var(--color-primary-dark);   /* 4.95 with white */
    --color-primary-fill-hover: #a8380f;                /* 6.49 with white */

    /* 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);

    /* Text on filled success and warning surfaces. Dark in both themes:
       white on these light hues reached only 2.36 and 2.16 : 1. */
    --color-success-ink: #1a1a1a;   /* 7.36, hover 6.26 */
    --color-warning-ink: #1a1a1a;   /* 8.08, hover 6.44 */

    --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;

    /* Filled danger surface with text. White on #ef5350 reaches 3.49 : 1 and on
       --color-error-dark still only 4.23; this red clears AA. */
    --color-error-fill: #c62828;         /* 5.62 with white */
    --color-error-fill-hover: #b71c1c;   /* 6.57 with white */

    --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;

    /* Opaque: a ring is a non-text indicator and needs 3 : 1 against the
       ground it sits on (WCAG 1.4.11). The former 40 % tint reached 1.68. */
    --focus-ring-color: var(--color-primary);
    --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.

   Screen only. Paper is white and drops backgrounds by default, so dark
   text tokens would print light grey on white — print always takes the
   light values above (REGELN.md §8). */
@media screen {
    [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 success and warning carry themselves — 6.24 and
           6.82 on their tint over a card, 5.51 and 6.03 over the page ground.
           Error falls short at 4.48 on a card and takes the lighter variant
           (5.22 / 4.64). Info passes on a card (4.90) but not on the page
           (4.35), where alerts and badges stand just as often — it takes the
           lighter variant too (6.92 / 6.14). */
        --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-light);

        /* 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 screen and (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-light);

        --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-fill);
    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%; }

/* Every filled button carries its label at 4.5 : 1 or more, in both themes
   (REGELN.md §5): brand and danger take a darker fill, success and warning a
   dark label. */
.btn-primary { background: var(--color-primary-fill); color: var(--color-primary-ink); border-color: var(--color-primary-fill); }
.btn-primary:hover:not(:disabled) { background: var(--color-primary-fill-hover); border-color: var(--color-primary-fill-hover); 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(--color-success-ink); 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(--color-warning-ink); 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-fill); color: var(--color-error-ink); border-color: var(--color-error-fill); }
.btn-danger:hover:not(:disabled) { background: var(--color-error-fill-hover); border-color: var(--color-error-fill-hover); }

.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;
}

/* Reordering cards by drag: the card in hand is dimmed, the card under the
   pointer shows where it will land — a bar on its left (`before`) or right
   (`after`) edge. A bar, not a dashed frame: dashed belongs to .dropzone (§7).
   The bar is a pseudo-element, not an inset shadow, so that a picture filling
   the card cannot cover it. */
.card.is-dragging { opacity: 0.5; }
.card.is-drop-target { position: relative; }
.card.is-drop-target[data-side]::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: 3px;
    background: var(--color-primary);
    pointer-events: none;
}
.card.is-drop-target[data-side="before"]::after { left: 0; }
.card.is-drop-target[data-side="after"]::after { right: 0; }

.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); }

/* Focus shows the global ring (02-base.css, 3 : 1) and the brand border.
   The former 40 % halo reached 1.68 : 1 and was the only cue. */
.form-control:focus { border-color: var(--color-primary); }

.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); }

/* 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); }

/* ===================================================================
   Disclosure

   Native <details>: the browser brings keyboard, state and announcement.
   The summary is a full tap target (§9); the content is whatever block the
   page puts after it — one gap below the summary, no trailing margin, so a
   .stack inside does not double up.
   =================================================================== */

.disclosure { margin-bottom: var(--spacing-md); }
.disclosure:last-child { margin-bottom: 0; }
.disclosure > summary {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    min-height: var(--button-height);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
    border-radius: var(--radius-sm);
    cursor: pointer;
    list-style: none;
}
.disclosure > summary::-webkit-details-marker { display: none; }
/* Chevron from two borders: points right when closed, down when open. */
.disclosure > summary::before {
    content: "";
    flex: none;
    width: 6px;
    height: 6px;
    margin: 0 var(--spacing-xs);
    border-right: 2px solid var(--text-secondary);
    border-bottom: 2px solid var(--text-secondary);
    transform: rotate(-45deg);
    transition: transform var(--transition-fast);
}
.disclosure[open] > summary::before { transform: rotate(45deg); }
.disclosure > summary:hover::before { border-color: var(--text-primary); }
.disclosure > summary + * { margin-top: var(--spacing-sm); }
.disclosure > :last-child:not(summary) { margin-bottom: 0; }

/* ===================================================================
   Info

   A short explanation beside a field or a result — a term and the norm it
   comes from. The native popover attribute brings the behaviour: the
   button opens and closes it (click, Enter, Space), Esc and a click
   elsewhere close it, and the browser reports the state as expanded. No
   script. A browser without popover shows the text inline. The popover
   follows its button in the markup, so it is read right after it.
   =================================================================== */

.info-btn {
    display: inline-grid;
    place-items: center;
    width: var(--spacing-lg);
    height: var(--spacing-lg);
    padding: 0;
    vertical-align: middle;
    color: var(--text-secondary);
    background: transparent;
    border: 0;
    border-radius: var(--radius-full);
    cursor: pointer;
}
.info-btn:hover { color: var(--text-primary); background: var(--hover-bg); }
/* Own rule: a browser without :popover-open would drop the hover with it. */
.info-btn:has(+ .info-pop:popover-open) { color: var(--text-primary); background: var(--hover-bg); }
/* After a field label the button stays on the label's line. */
.form-label:has(+ .info-btn) { display: inline-block; }

/* Under a thumb the target grows to 40 px (REGELN.md §9); the negative
   margin keeps the line it sits in from growing with it. */
@media (pointer: coarse) {
    .info-btn {
        width: var(--button-height);
        height: var(--button-height);
        margin-block: calc((var(--spacing-lg) - var(--button-height)) / 2);
    }
}

/* No display here: the UA sheet hides a closed popover, and an author
   display would show it closed. Text styles are reset because a popover
   inherits from where it sits — a bold, unwrapped table head, a label.
   The minimum width matters for the placement below: without it the text
   shrinks into the strip between button and screen edge (measured: 175 px
   on a phone) instead of flipping or taking the full width. */
.info-pop {
    min-width: min(240px, calc(100vw - 2 * var(--spacing-md)));
    max-width: min(320px, calc(100vw - 2 * var(--spacing-md)));
    padding: var(--spacing-sm) var(--spacing-md);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-normal);
    line-height: var(--line-height-normal);
    letter-spacing: var(--letter-spacing-normal);
    text-align: left;
    text-transform: none;
    white-space: normal;
    color: var(--text-primary);
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
}

/* Beside its button where anchor positioning exists (the button is the
   popover's implicit anchor); flipped where there is no room, across the
   width on a narrow screen. Elsewhere the browser centres it. */
@supports (position-area: block-end) {
    .info-pop {
        inset: auto;
        /* The side margin keeps a flipped text off the screen edge: where
           16 px no longer fit beside the button, it takes the full width. */
        margin: var(--spacing-xs) var(--spacing-md);
        position-area: block-end span-inline-end;
        position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline, block-end span-all, block-start span-all;
    }
}

/* ===================================================================
   Before / after

   A changed value in place. The words carry the meaning: most screen
   readers do not announce <del> and <ins>, and colour alone must not
   (REGELN.md §3). No status hue either — a removed value is not an error
   and a new one not a success. Old recedes and is struck through, new is
   set off; both survive black-and-white print.
   =================================================================== */

.diff {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--spacing-2xs) var(--spacing-sm);
    max-width: 100%;
}
.diff__part { min-width: 0; overflow-wrap: anywhere; }
.diff__label { font-size: var(--font-size-xs); color: var(--text-secondary); }
.diff del { color: var(--text-secondary); text-decoration: line-through; }
.diff ins {
    padding: 0 var(--spacing-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--text-primary);
    text-decoration: none;
    background: var(--bg-tertiary);
    border-radius: var(--radius-sm);
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
}

/* ===================================================================
   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); }
}

/* ===================================================================
   Print

   Paper is white and drops backgrounds unless asked to keep them. So print
   always takes the light theme (01-tokens.css keeps dark to the screen),
   nothing relies on a tint, and controls stay on the screen. Paper size
   and margins belong to the print dialog — A4 is its default here.
   =================================================================== */

/* The head of a printed result: what it is, which version, as of when,
   printed when. Only on paper; the page fills in the time, e.g. on
   `beforeprint`. */
.print-header {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--spacing-xs) var(--spacing-lg);
    margin-bottom: var(--spacing-lg);
    padding-bottom: var(--spacing-sm);
    border-bottom: 1px solid var(--border-color-dark);
}
.print-header__title { margin: 0; font-size: var(--font-size-xl); font-weight: var(--font-weight-semibold); }
.print-header__meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-xs) var(--spacing-md);
    margin: 0;
    font-size: var(--font-size-xs);
    color: var(--text-secondary);
}
.print-header__meta > div { display: flex; gap: var(--spacing-xs); }
.print-header__meta dt { font-weight: var(--font-weight-medium); }
.print-header__meta dd { margin: 0; color: var(--text-primary); }

@media not print {
    .print-header,
    .print-only { display: none !important; }
}

@media print {
    body { background: var(--bg-primary); }

    .no-print,
    .navbar,
    .tabs,
    .btn,
    .dropzone,
    .info-btn,
    .map__tools,
    .modal-overlay,
    dialog.modal { display: none !important; }
    /* Own rule: an unknown pseudo-class would void the whole list above. */
    .info-pop:popover-open { display: none; }

    /* Wide content wraps on paper; a scroll box would cut it off. The
       header row repeats on every page by itself (thead). */
    .table-scroll,
    pre { overflow: visible; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
    .table th,
    .table--nowrap th,
    .table--nowrap td { white-space: normal; }
    /* Only value cells break inside a word: on a header or the 1 % key
       column the column would collapse to one letter. */
    .table td { overflow-wrap: anywhere; }

    .table tr,
    .alert,
    .stat-card,
    .list-item,
    .print-header { break-inside: avoid; }
    h1, h2, h3, h4, h5, h6 { break-after: avoid; }

    /* A state drawn as a fill — the status dot, a ticked box, a switch —
       prints even without background graphics. */
    .badge__dot,
    .form-check-input:checked,
    .form-switch-input { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

/* ===================================================================
   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;
}
