/* ==========================================================================
   Breuer App — Design System
   Structure and form language follow dtf-express-hub: everything operable
   lives in the sidebar (navigation on top, user card at the bottom), page
   titles in the content area, soft radii (8 px on controls, 16 px on cards),
   a humanist sans in normal case — no uppercase, no letter-spacing.
   Colors: light-gray surface, white cards, text #212121. Blue (#009cd8,
   the dominant logo color) used exclusively as an accent.
   ========================================================================== */

:root {
    /* Accent — only for actions, active states and highlights.

       Since Feedback #13114 the three tones come from a PRESET
       (`html[data-akzent="…"]`, further down); what is set here is the
       shipped default „Breuer-Blau" and at the same time the fallback
       for the case that no accent is set at all.

       ⚠️ `--accent-rgb` is the triplet WITHOUT the surrounding `rgb()`. Only
       this way can `--accent-soft` and `--accent-line` be defined ONCE
       instead of again in every preset — and only this way does the glass
       theme's gradient follow the chosen accent instead of staying blue. */
    --accent-rgb: 0, 156, 216;
    --accent: #009cd8;
    --accent-bright: #18a9d9;
    --accent-dark: #0079a8;
    --accent-soft: rgba(var(--accent-rgb), 0.10);
    --accent-line: rgba(var(--accent-rgb), 0.30);

    /* Signal colors (gradient in the logo mark) — used sparingly for status */
    --signal-orange: #e8850a;
    --signal-red: #c1391b;
    --signal-green: #2f8f57;
    /* Only for the assignment type „Kundendienst": a FIXED blue — the
       accent can be switched to another colour, this must not follow. */
    --signal-blue: #1f6fb8;
    /* The same three as TEXT on their own light tint
       (badges, notice lines). Until Feedback #19232 they stood in every
       rule as a hex value — dark mode (`dunkel.css`) needs them light,
       and that only works from one place. `-tief` is the slightly darker
       orange of two badges that already carried it before. */
    --signal-green-text: #226b41;
    --signal-orange-text: #9a5806;
    --signal-orange-text-tief: #8a4e05;
    --signal-red-text: #8f2b14;
    --signal-blue-text: #17548c;

    /* Surfaces — light gray and border from dohmen-sensorview
       (Tailwind primary-50 / primary-100), so the apps feel consistent. */
    --bg: #f5f7f8;
    --surface: #ffffff;
    --surface-hover: #eef1f2;
    --border: #e8ecee;
    --border-strong: #d2d9dc;
    /* ⚠️ The same surfaces, but OPAQUE — and deliberately named so that
       the glass theme does not touch them. The preview tiles in the
       „Darstellung" picker (Feedback #13114) need them: the „Standard" tile
       must look opaque even while the app is currently glassy. */
    --paper: #ffffff;
    --paper-line: #e8ecee;
    /* The ground under a QR code — white in EVERY mode, which is why
       `dunkel.css` deliberately has no counterpart (Feedback #19575). The
       code draws black modules on a transparent ground; on a dark card that
       is black on black, and inverting it is no way out — many scanners do
       not read light-on-dark codes. */
    --qr-grund: #ffffff;
    /* The tint of the frosted glass as a triplet WITHOUT `rgb()` — same
       pattern as `--accent-rgb`. Every glassed SURFACE is
       `rgba(var(--glas-rgb), a)`, so dark mode (`dunkel.css`, Feedback
       #19232) can tint the glass dark with ONE line instead of writing
       every glass rule a second time. The light EDGES (light rim, borders)
       stay white — `dunkel.css` dims those individually. */
    --glas-rgb: 255, 255, 255;

    /* Text */
    --text: #212121;
    --text-muted: #5c5c5c;
    /* 4.9:1 on white — the small text stays readable */
    --text-dim: #6f6f6f;

    /* Structure */
    --sidebar-w: 256px;   /* w-64 */
    /* Width of the COLLAPSED rail (Feedback #13098). 72 px is the
       number from pvwebapp (`COLLAPSED_WIDTH` in `MiniDrawer.js`) — it
       carries a 32-px icon with breathing room and lets the user card fit
       two of them side by side. */
    --sidebar-rail: 72px;
    /* like dtf-express: rounded-lg on controls, rounded-2xl on cards */
    --radius: 8px;
    --radius-lg: 16px;
    /* Inner left/right edge of EVERY card: padded cards, the head of a flush
       card and the first/last table column. One value, so headings of
       stacked cards line up (mobile 16 px, see the 860-px block). */
    --card-inset: 24px;

    /* System font like dtf-express — deliberately no webfont */
    --font-body: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto,
                 sans-serif;

    --shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
    --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.05);
}

/* ==========================================================================
   Appearance — accent color and background (Feedback #13114)
   ==========================================================================
   Two further axes besides the theme, modeled on pvwebapp
   (`styles/appearance.css@release`: one preset block per value that only
   sets variables). Four decisions carry this section:

   1. **Only VARIABLES, no second copy of the rules.** Same pattern as
      `--glas`/`--glanz`/`--flaeche` in the glass theme and for the same
      reason: whatever runs through the variables falls into every
      fallback too — print, `prefers-reduced-transparency`, old browsers.
   2. **A preset also colors its own PREVIEW.** Every block applies to
      `html[data-akzent="x"]` AND to `.swatch--x` resp. `.ground--x`. The
      color wheel in the picker thus literally shows the same values it
      picks; a preview with its own hex values would drift away from that
      at the next refinement.
   3. **The block comes BEFORE the glass theme.** `html[data-grund="…"]`
      and `html[data-thema="glass"]` have the same specificity — on a tie
      order decides. This keeps the surfaces translucent in glass mode,
      and the background colors what is seen through them. Reversed,
      glass would no longer be glass.
   4. **Every background here is LIGHT.** Its dark counterpart lives
      in `dunkel.css` and applies when the user has chosen „Dunkel" or
      „Automatisch" (Feedback #19232) — reasoning in
      `app/appearance.py`.
   ========================================================================== */

/* --- Accent colors ---------------------------------------------------------
   `--accent-dark` carries text on a light background (links, badges) and
   is above 4.8:1 for all eight. `--accent` carries WHITE text; the
   weakest allowed pairing is the logo blue at 3.11:1 — a new tone is
   therefore never chosen lighter than that.
   -------------------------------------------------------------------------- */

html[data-akzent="blau"],
.swatch--blau {
    --accent-rgb: 0, 156, 216;
    --accent: #009cd8;
    --accent-bright: #18a9d9;
    --accent-dark: #0079a8;
}

html[data-akzent="petrol"],
.swatch--petrol {
    --accent-rgb: 13, 127, 140;
    --accent: #0d7f8c;
    --accent-bright: #258c98;
    --accent-dark: #0a636d;
}

html[data-akzent="marine"],
.swatch--marine {
    --accent-rgb: 31, 78, 140;
    --accent: #1f4e8c;
    --accent-bright: #356098;
    --accent-dark: #183d6d;
}

html[data-akzent="indigo"],
.swatch--indigo {
    --accent-rgb: 76, 81, 191;
    --accent: #4c51bf;
    --accent-bright: #5e62c5;
    --accent-dark: #3b3f95;
}

html[data-akzent="violett"],
.swatch--violett {
    --accent-rgb: 109, 84, 224;
    --accent: #6d54e0;
    --accent-bright: #7c65e3;
    --accent-dark: #5542af;
}

html[data-akzent="beere"],
.swatch--beere {
    --accent-rgb: 181, 70, 138;
    --accent: #b5468a;
    --accent-bright: #bc5896;
    --accent-dark: #8d376c;
}

html[data-akzent="terra"],
.swatch--terra {
    --accent-rgb: 209, 96, 47;
    --accent: #d1602f;
    --accent-bright: #d67044;
    --accent-dark: #a34b25;
}

html[data-akzent="graphit"],
.swatch--graphit {
    --accent-rgb: 74, 85, 104;
    --accent: #4a5568;
    --accent-bright: #5c6677;
    --accent-dark: #3a4251;
}

/* --- Backgrounds -----------------------------------------------------------
   `hellgrau` is the tone the app started with — it is spelled out here
   rather than as „do nothing", so that its tile in the
   picker also has a preview.

   In the glass theme only `--bg` takes effect from these blocks: the
   other surfaces stay translucent there (point 3 above). That is
   exactly the intent — the background is what is seen through the glass.
   -------------------------------------------------------------------------- */

html[data-grund="hellgrau"],
.ground--hellgrau {
    --bg: #f5f7f8;
    --surface: #ffffff;
    --surface-hover: #eef1f2;
    --border: #e8ecee;
    --border-strong: #d2d9dc;
    --paper: #ffffff;
    --paper-line: #e8ecee;
}

html[data-grund="schnee"],
.ground--schnee {
    --bg: #fbfcfd;
    --surface: #ffffff;
    --surface-hover: #f1f4f6;
    --border: #e9edf0;
    --border-strong: #d5dbe0;
    --paper: #ffffff;
    --paper-line: #e9edf0;
}

html[data-grund="sand"],
.ground--sand {
    --bg: #faf5ec;
    --surface: #fffdf9;
    --surface-hover: #f4ede0;
    --border: #ece3d3;
    --border-strong: #d8ccb6;
    --paper: #fffdf9;
    --paper-line: #ece3d3;
}

html[data-grund="nebel"],
.ground--nebel {
    --bg: #eceff3;
    --surface: #ffffff;
    --surface-hover: #e6eaef;
    --border: #e0e5ea;
    --border-strong: #cbd2da;
    --paper: #ffffff;
    --paper-line: #e0e5ea;
}

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

/* Must take effect after the component rules: otherwise a class's
   `display: flex` would beat the `hidden` attribute, and the modal and
   menu backdrop would stand open on load. */
[hidden] {
    display: none !important;
}

html,
body {
    margin: 0;
    padding: 0;
}

body {
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 14px;
    line-height: 22px;
    -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4 {
    font-weight: 700;
    margin: 0;
}

a {
    color: var(--accent-dark);
    text-decoration: none;
}

a:hover {
    color: var(--accent);
}

/* --------------------------------------------------------------------------
   Login
   -------------------------------------------------------------------------- */

.login-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 32px 20px;
    background: var(--bg);
}

.login-card {
    width: 100%;
    max-width: 420px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: 42px 40px 34px;
    text-align: center;
}

.login-card .logo {
    width: 116px;
    height: auto;
    margin-bottom: 24px;
}

.login-card h1 {
    font-size: 24px;
    line-height: 1.25;
    margin-bottom: 6px;
}

.login-card .subline {
    color: var(--text-dim);
    font-size: 13px;
    margin: 0 0 28px;
}

.login-hint {
    color: var(--text-muted);
    font-size: 13px;
    line-height: 21px;
    margin: 0 0 20px;
}

/* The designated path to the platform login (#15100). It appears only
   once the employee button reports „no active login" — hence a block of
   its own below the button, not part of the form. */
.login-weiter {
    display: block;
    margin: 14px 0 0;
    /* `--accent-dark` is the text color for links on a light background (see
       the color table above). In `--text-muted` the link looked like a
       hint, and nobody would have clicked it. */
    color: var(--accent-dark);
    font-size: 12px;
    line-height: 19px;
    text-align: center;
    text-decoration: underline;
}

.login-weiter[hidden] {
    display: none;
}

/* The customer booking page (Feedback #13600) — same card as the login,
   the day selection left-aligned: a centered list of radio buttons reads
   like a poem, not like a choice. */
.buchung__tage {
    border: 0;
    margin: 0 0 16px;
    padding: 0;
    text-align: left;
}

.buchung__tag {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    margin-top: 6px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    cursor: pointer;
}

.buchung__tag:has(input:checked) {
    border-color: var(--accent);
}

.buchung .field {
    text-align: left;
}

.login-card .meta {
    margin: 24px 0 0;
    padding-top: 18px;
    border-top: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 12px;
    line-height: 19px;
}

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    width: 100%;
    padding: 11px 20px;
    background: var(--accent);
    color: #ffffff;
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.18s ease, border-color 0.18s ease;
}

.btn:hover {
    background: var(--accent-bright);
    border-color: var(--accent-bright);
    color: #ffffff;
}

.btn:focus-visible {
    outline: 2px solid var(--accent-dark);
    outline-offset: 2px;
}

/* Width only as large as needed — for button rows */
.btn--auto {
    width: auto;
}

.btn--secondary {
    width: auto;
    background: var(--surface);
    color: var(--text-muted);
    border-color: var(--border-strong);
}

.btn--secondary:hover {
    background: var(--surface-hover);
    border-color: var(--border-strong);
    color: var(--text);
}

/* Icon button — user card, menu, modal */
.iconbtn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    padding: 0;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius);
    color: var(--text-dim);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.iconbtn:hover {
    background: var(--surface-hover);
    color: var(--text);
}

.iconbtn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.iconbtn.is-active {
    background: var(--accent-soft);
    color: var(--accent-dark);
}

.iconbtn svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    stroke-width: 1.7;
}

.iconbtn--sm {
    width: 28px;
    height: 28px;
}

.iconbtn__badge {
    position: absolute;
    top: 2px;
    right: 2px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 9999px;
    background: var(--signal-red);
    color: #ffffff;
    font-size: 10px;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
}

/* --------------------------------------------------------------------------
   App shell
   -------------------------------------------------------------------------- */

.shell {
    display: flex;
    min-height: 100vh;
}

.sidebar {
    width: var(--sidebar-w);
    flex-shrink: 0;
    background: var(--surface);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    height: 100vh;
    /* If the content does not fit (all groups open), the rail scrolls itself.
       Without this it simply stuck out past the bottom edge — and because
       the rail is `sticky` with a fixed height, no page scroll brought it
       back either: the user card with bell, settings and logout was
       unreachable (Feedback #12851). This is a question of HEIGHT, not of
       width — measured on a 1440x900 laptop just as on the phone, so the
       rule lives here and not in the mobile block. If the content fits, no
       scrollbar appears and nothing changes. */
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}

.sidebar__brand {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 16px;
    border-bottom: 1px solid var(--border);
    color: var(--text);
    transition: background 0.15s ease;
}

.sidebar__brand:hover {
    background: var(--surface-hover);
    color: var(--text);
}

.sidebar__brand img {
    height: 34px;
    width: auto;
}

.sidebar__brand span {
    flex: 1;
    min-width: 0;
    font-size: 14px;
    font-weight: 700;
}

/* Pin/unpin — sits in the head of the rail, like in pvwebapp. It
   only appears from 861 px: below that the rail is an overlay and there
   is nothing to pin. */
.sidebar__pin {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    flex-shrink: 0;
    background: none;
    border: none;
    border-radius: var(--radius);
    color: var(--text-dim);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.sidebar__pin:hover {
    background: var(--surface-hover);
    color: var(--text);
}

/* Pinned, the button is ON — the same statement as `aria-pressed="true"`,
   just visible. Without the coloring the button would not show which of
   the two states the rail is currently in. */
.sidebar__pin[aria-pressed="true"] {
    color: var(--accent);
}

.sidebar__pin svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    stroke-width: 1.8;
}

.sidebar__close {
    display: none;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    background: none;
    border: none;
    border-radius: var(--radius);
    color: var(--text-dim);
    cursor: pointer;
}

.sidebar__close svg {
    width: 18px;
    height: 18px;
    stroke: currentColor;
    fill: none;
    stroke-width: 1.8;
}

.sidebar__backdrop {
    display: none;
}

.nav {
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    flex: 1;
    overflow-y: auto;
}

.nav a {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 8px 12px;
    border-radius: var(--radius);
    color: var(--text-muted);
    font-size: 14px;
    border-left: 2px solid transparent;
    transition: background 0.15s ease, color 0.15s ease;
}

.nav a:hover {
    background: var(--surface-hover);
    color: var(--text);
}

.nav a.is-active {
    background: var(--accent-soft);
    border-left-color: var(--accent);
    color: var(--text);
    font-weight: 600;
}

.nav a svg {
    width: 17px;
    height: 17px;
    flex-shrink: 0;
    stroke: currentColor;
    fill: none;
    stroke-width: 1.7;
}

.nav a.is-active svg {
    stroke: var(--accent);
}

/* Counter on the menu entry — today the open leave requests (Feedback
   #13007). `margin-left: auto` pushes it to the right edge of the row, so
   it doesn't stick to the text and stays in the same place for labels of
   different length. Same shape as the badge on the bell. */
.nav__count {
    margin-left: auto;
    flex-shrink: 0;
    min-width: 18px;
    padding: 0 5px;
    border-radius: 9999px;
    background: var(--signal-red);
    color: #ffffff;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
}

/* Head of a menu group („Organisation", „Verwaltung"): groups the entries
   AND collapses them (Feedback #12818). The whole row is the button, not
   just the chevron — a 14-px arrow is not a target you can hit. */
.nav__toggle {
    display: flex;
    align-items: center;
    gap: 11px;
    width: 100%;
    margin: 0;
    /* 14 px left = 12 px padding + the 2-px accent edge of `.nav a`: the
       group icon stands exactly above the icons of the entries. */
    padding: 8px 12px 8px 14px;
    background: none;
    border: none;
    border-radius: var(--radius);
    /* A bit stronger than --text-dim, since the label got bigger
       (Feedback #12851). The color deliberately lives HERE and not on
       `.nav__label`: there it would beat the `.is-active` coloring below,
       and the collapsed group head would no longer show where you are. */
    color: var(--text-muted);
    text-align: left;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.nav__toggle:hover {
    background: var(--surface-hover);
    color: var(--text);
}

/* If the open page sits in a COLLAPSED group, its entry is not visible —
   then at least the head must show where you are. Collapsed stays
   collapsed: a group that reopens itself on navigation makes the toggle
   worthless. */
.nav__toggle.is-active {
    color: var(--accent-dark); /* --accent was below 4.5:1 in dark mode */
}

/* The group label. Was 11 px (unreadable on the phone, Feedback #12851), then
   13 px below the entries (14 px); now 15 px on the operator's request — the
   head is told apart from its entries by weight and icon, not by being
   smaller. `margin-right: auto` pushes counter and chevron to the edge. */
.nav__label {
    margin-right: auto;
    font-size: 15px;
    font-weight: 700;
    letter-spacing: 0.01em;
}

/* The icon of a group head — always shown (operator's request), in the
   collapsed rail it stands in for the label. Same size as the entries'. */
.nav__groupicon {
    display: block;
    width: 17px;
    height: 17px;
    flex-shrink: 0;
    stroke: currentColor;
    fill: none;
    stroke-width: 1.7;
}

.nav__chevron {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
    transition: transform 0.15s ease;
}

/* Collapsed, the chevron points right. */
.nav__toggle[aria-expanded="false"] .nav__chevron {
    transform: rotate(-90deg);
}

.nav__items {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

/* `hidden` alone is not enough — the `display: flex` above it wins. */
.nav__items[hidden] {
    display: none;
}

/* The main navigation does NOT grow along: the free space belongs to the
   organization block below it, so it directly follows dashboard and
   feedback instead of sliding to the bottom. If it doesn't exist (nobody
   has an organization permission), `.userbar { margin-top: auto }` still
   keeps the user card at the bottom. */
.nav--main {
    flex: 0 0 auto;
    overflow: visible;
}

/* Feedback as the last entry of the structure, right above the
   user card (Feedback #12818). */
.nav--last {
    flex: 0 0 auto;
    padding: 0 12px 12px;
    overflow: visible;
}

/* User card at the bottom of the rail */

.userbar {
    display: flex;
    align-items: center;
    gap: 4px;
    border-top: 1px solid var(--border);
    padding: 12px;
    /* Stays at the bottom, even if nothing above it grows. */
    margin-top: auto;
}

.userbar__id {
    flex: 1;
    min-width: 0;
    padding: 4px 8px;
}

.userbar__name,
.userbar__role {
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.userbar__name {
    font-size: 14px;
    font-weight: 500;
    line-height: 20px;
}

.userbar__role {
    font-size: 12px;
    line-height: 16px;
    color: var(--text-dim);
}

/* Main area */

.main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.mobilebar {
    display: none;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

.mobilebar img {
    height: 26px;
    width: auto;
}

.mobilebar span {
    font-size: 15px;
    font-weight: 700;
}

.content {
    flex: 1;
    padding: 24px 72px; /* 72 = .fabs right 24 + button 36 + 12 air */
}

/* Page head in the content — replaces the former top bar */
.pagehead {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 24px;
}

/* Title left, ALL actions grouped on the right. With `space-between` alone a
   page with two buttons (Personal) put the first one in the middle. */
.pagehead > :first-child {
    margin-right: auto;
}

/* ⚠️ The headings are ONE step larger than the body-text canon of
   dtf-express (Feedback #14120): page title 28 px, section head 18 px.
   It was reported on the project file, changed here — both are the only
   definitions of their level, so every page matches. */
.pagehead h1 {
    font-size: 28px;
    line-height: 1.25;
}

.pagehead__sub {
    margin: 4px 0 0;
    font-size: 13px;
    color: var(--text-dim);
}

/* --------------------------------------------------------------------------
   Cards, empty state, data list
   -------------------------------------------------------------------------- */

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
}

.card--pad {
    padding: 24px var(--card-inset);
}

/* Bold on its own: 102 of the 125 section heads are a `<p>`, which carries
   no weight — „Positionen" stood thin next to a bold „Konto". */
.section-title {
    font-size: 18px;
    font-weight: 700;
    margin-bottom: 14px;
}

.datalist {
    display: grid;
    grid-template-columns: minmax(0, 190px) minmax(0, 1fr);
    gap: 7px 20px;
    margin: 0;
    font-size: 13px;
}

.datalist dt {
    color: var(--text-dim);
}

.datalist dd {
    margin: 0;
    color: var(--text);
}

.hint {
    margin: 20px 0 0;
    padding-top: 16px;
    border-top: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 12px;
    line-height: 19px;
}

/* Hint below a card instead of inside it — then without a divider */
.hint--standalone {
    border-top: none;
    padding-top: 0;
    margin: 14px 2px 0;
    max-width: 760px;
}

/* Empty area with a reason why nothing is here — modeled on the
   EmptyState from dtf-express-hub. */
.empty-state {
    padding: 62px 32px;
    text-align: center;
}

.empty-state__icon {
    width: 56px;
    height: 56px;
    margin: 0 auto 16px;
    border-radius: 9999px;
    background: var(--accent-soft);
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Without a fixed size and `fill: none` the browser renders the icon at
   its default size and fills the paths black — it then fills the whole
   card. This rule is not cosmetic. */
.empty-state__icon svg {
    width: 25px;
    height: 25px;
    flex-shrink: 0;
    /* accent-dark instead of accent: on the light-blue tint the lighter tone
       reaches too little contrast and the icon blends with the surface. */
    stroke: var(--accent-dark);
    fill: none;
    stroke-width: 1.6;
}

/* Same level as `.section-title` — the empty state sits in the same
   place as a section and must not drift away from it. */
.empty-state h2 {
    font-size: 18px;
    margin-bottom: 6px;
}

.empty-state p {
    color: var(--text-dim);
    font-size: 13px;
    max-width: 400px;
    margin: 0 auto;
}

/* --------------------------------------------------------------------------
   Table — structure like the user list in dtf-express-hub
   -------------------------------------------------------------------------- */

/* ⚠️ `overflow-x: auto`, NOT `hidden` — and that at EVERY width (Feedback
   #13056). The card clips the corners of a borderless table here; with
   `hidden` it cut off everything wider than itself, WITHOUT any way to
   scroll. The right-hand action column was always affected:
   „Bearbeiten", „Archivieren", „Erledigt", „Korrigieren" were not just
   small, but unreachable (measured /kunden 171 px at 1280 px viewport,
   551 px at 900 px; /aufgaben, /lager, /produkte likewise).

   The fix was already there — but in `@media (max-width: 860px)`, so only
   for phones. That was exactly the bug: a table does not only get too
   wide on the phone, but whenever its content is wider than its box —
   and the box is narrow next to the 256-px sidebar even on a laptop.
   `auto` still clips the corners and adds a scrollbar below as soon as
   (and only when) it is needed. */
.card--flush {
    overflow-x: auto;
    overflow-y: hidden;
    /* A thin but VISIBLE bar. „Erreichbar" (reachable) and „auffindbar"
       (findable) are two different things: a user who doesn't see that it
       continues to the right won't look for the button — they'll report it
       as missing. Both values only take effect if a bar is needed at all;
       `--border-strong` follows the theme (standard as well as glass). */
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}

.table {
    width: 100%;
    border-collapse: collapse;
    text-align: left;
    font-size: 14px;
}

/* Header row: light-gray background, small and uppercase — the only
   place where dtf-express uses uppercase. */
.table thead {
    background: var(--bg);
    border-bottom: 1px solid var(--border);
}

.table thead th {
    padding: 12px 16px;
    font-size: 12px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--text-dim);
    white-space: nowrap;
}

.table tbody td {
    padding: 12px 16px;
    vertical-align: middle;
}

/* Dividers only BETWEEN the rows (divide-y), not below the last one */
.table tbody tr + tr td {
    border-top: 1px solid var(--border);
}

.table__name {
    margin: 0;
    font-weight: 500;
    color: var(--text);
}

.table__sub {
    margin: 0;
    font-size: 12px;
    line-height: 17px;
    color: var(--text-dim);
}

/* ⚠️ Only the COLOR, no more `white-space` (Feedback #13056). The class
   says „muted" — it says nothing about whether the content must stay
   together, and carrying both in one class went wrong both ways:

   * It sits on FREE-TEXT cells (address, note, decline reason, comment).
     Without wrapping, the address made the city column of the customer
     list 305 px wide — the table thereby blew its box by exactly the
     63 px it was missing on a 1280-px screen.
   * It also sits on whole `<tr>` rows (customer file, project file) — as
     a pure coloring of a completed row. There `nowrap` was inherited by
     EVERY cell of the row, which was never the intent.

   What really must stay together is now said by `.table__nowrap`. */
.table__muted {
    color: var(--text-muted);
}

/* A value that is only readable as a whole: „08:00–16:00" (the dash is a
   wrap opportunity), „25.08.2026, 19:16", „5 Tage". A plain date doesn't
   need this — „25.08.2026" has no spot to wrap at. */
.table__nowrap {
    white-space: nowrap;
}

/* Status */

.badge {
    display: inline-block;
    padding: 2px 9px;
    border-radius: 9999px;
    font-size: 12px;
    font-weight: 500;
    white-space: nowrap;
}

.badge--active {
    background: var(--accent-soft);
    color: var(--accent-dark);
}

.badge--blocked {
    background: var(--border);
    color: var(--text-muted);
}

/* „Alles abgehakt" (all checked, Feedback #13418). Green is this app's
   color for „erledigt" (done); a fully checked counter is the only place
   where a checklist REPORTS anything at all. Dark tone on a light tint
   like the other badges — a dark background is deliberately not
   provided anywhere. */
.badge--ok {
    background: rgba(47, 143, 87, 0.13);
    color: var(--signal-green);
}

/* The handle for reordering (Feedback #16045).

   ⚠️ `touch-action: none` is not cosmetic: without it the browser treats
   the swipe gesture as scrolling, takes the pointer away from us after a
   few pixels (`pointercancel`), and the row stays put — on the phone
   dragging would then not be usable at all. It deliberately sits only on
   the HANDLE: the rest of the page should keep scrolling normally. */
.btn--griff {
    cursor: grab;
    touch-action: none;
    /* The handle glyph ⠿ is narrower than a label — without a fixed
       width the button would be half the size of its neighbors and thus a
       poor target for the finger. */
    min-width: 2.2rem;
    /* The handle glyph is a braille pattern and at button size otherwise
       renders as two barely visible rows of dots — but it should read as
       „grab here" at a glance. */
    font-size: 1.05rem;
    line-height: 1;
    letter-spacing: 0.08em;
}

.btn--griff:active,
.is-ziehend .btn--griff {
    cursor: grabbing;
}

/* The row currently hanging off the mouse: raised, so you can see WHAT
   you're dragging, and where it would currently land. */
tr.is-ziehend {
    background: var(--surface-hover);
    box-shadow: 0 6px 14px rgba(15, 23, 42, 0.16);
    opacity: 0.92;
}

/* Select nothing while dragging — otherwise the pointer colors half the
   table blue instead of moving the row. */
tbody.is-sortierend {
    user-select: none;
}

/* Row actions: on the desktop visible only on hover, on touch devices
   permanently (there is no hover there). For the keyboard, focus-within
   fades them in. */
.rowactions {
    display: flex;
    /* ⚠️ Wrap, don't overflow (Feedback #13056). `.btn--sm` carries
       `flex: 0 0 auto` — so the buttons don't shrink. Without `wrap` a row
       was as wide as its labels combined, no matter how narrow the
       screen was: the four header buttons of the customer file measured
       439 px on a phone at a 390-px viewport, „Projekt anlegen" stuck out
       on the left and „Archivieren" on the right. Two rows are uglier
       than one — a button you can't see is worse. */
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 2px;
}

@media (hover: hover) and (min-width: 861px) {
    .rowactions {
        opacity: 0;
        transition: opacity 0.15s ease;
    }

    tr:hover .rowactions,
    tr:focus-within .rowactions,
    tr.is-ziehend .rowactions {
        opacity: 1;
    }

    /* In a MANAGEMENT dialog the buttons ARE the content, not the
       accessory of a data row — there „appears on hover" would be exactly
       the searching the dialog is meant to abolish. */
    .rowactions--fixed {
        opacity: 1;
    }
}

/* Columns that are dropped on narrow viewports */
@media (max-width: 700px) {
    .col-sm {
        display: none;
    }
}

@media (min-width: 701px) {
    .col-sm-only {
        display: none;
    }
}

@media (max-width: 900px) {
    .col-md {
        display: none;
    }
}

/* Feedback above a list */
.notice {
    margin: 0 0 14px;
    padding: 10px 14px;
    border-radius: var(--radius);
    font-size: 13px;
}

.notice--ok {
    background: var(--accent-soft);
    color: var(--accent-dark);
}

.notice--error {
    background: rgba(193, 57, 27, 0.10);
    color: var(--signal-red);
}

/* Something isn't quite right, but nothing has gone wrong — the day plan
   without its own assignment (Feedback #13144). Deliberately NOT
   `--error`: the fallback works, it's just not what was meant. */
.notice--warn {
    background: rgba(232, 133, 10, 0.12);
    color: var(--signal-orange-text);
}

/* Information without a judgment — the route plan above the maintenance
   list. Neither success nor error, hence neither accent nor signal color. */
.notice--quiet {
    background: var(--border);
    color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   My Day — the next appointment large (Feedback #13144, #19230)
   --------------------------------------------------------------------------
   The page is opened on the phone in the morning, and the only question
   is „where do I need to go first?". That's why the first appointment
   stands as its own card above the list and carries the app's largest
   font — it should be readable without scrolling and without searching.
   -------------------------------------------------------------------------- */

.tagesplan__pfeile {
    margin-bottom: 16px;
}

.tagesplan__erster {
    border-left: 3px solid var(--accent);
}

.tagesplan__zeit {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 4px;
    font-size: 30px;
    font-weight: 700;
    line-height: 1.1;
}

.tagesplan__titel {
    margin: 0 0 2px;
    font-size: 17px;
    font-weight: 600;
}

.tagesplan__kunde,
.tagesplan__ort {
    margin: 0 0 4px;
    font-size: 14px;
}

.tagesplan__ort {
    color: var(--text-muted);
}

/* Info icon next to „Wo" (where), when it is only the customer address
   (Feedback #18821). `<details>` instead of tooltip: click and tap
   instead of hover. The text expands IN the cell, not as a floating
   bubble — a table scrolling sideways on narrow windows would clip it. */
.wohinweis {
    display: inline;
}

.wohinweis__knopf {
    display: inline-flex;
    vertical-align: -3px;
    margin-left: 4px;
    color: var(--accent);
    cursor: pointer;
    list-style: none;
}

.wohinweis__knopf::-webkit-details-marker {
    display: none;
}

.wohinweis__knopf:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    border-radius: 50%;
}

.wohinweis__knopf svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    stroke-width: 1.7;
}

.wohinweis__text {
    margin: 6px 0 0;
    padding: 6px 8px;
    font-size: 12px;
    line-height: 17px;
    color: var(--text);
    background: var(--accent-soft);
    border-radius: var(--radius);
}

/* Completed items in the day plan (Feedback #15834): collapsed below the
   open tasks, expandable without JavaScript. Deliberately unobtrusive —
   it's a record, not work, and must not outshine the open list. */
.tagesplan__klappe {
    border-top: 1px solid var(--border);
}

.tagesplan__klappkopf {
    padding: 10px 16px;
    color: var(--text-muted);
    font-size: 13px;
    cursor: pointer;
}

.tagesplan__fertig {
    margin: 0;
    padding: 14px 16px;
    color: var(--text-muted);
}

/* The checked-off title. The button inside it is its own box
   (`inline-block`) — the field's strikethrough doesn't reach it, it has
   to carry it itself. The badge next to it stays unstruck for the same
   reason, and rightly so: „Erledigt" is the row's statement. */
.table__name--erledigt,
.table__name--erledigt .linkbtn {
    color: var(--text-dim);
    text-decoration: line-through;
}

/* The person picker for the lead sits to the right in the paging bar and
   wraps with it on the phone (`.monthbar` is `flex-wrap`). */
.tagesplan__wer {
    min-width: 0;
    max-width: 100%;
}

.tagesplan__wer .field__input {
    max-width: 100%;
}

/* Calendar: week/month and employee to the right in the bar (Feedback
   #18596). Wraps like `.monthbar` itself — on the phone the picker slides
   below the arrows instead of letting the page scroll sideways. */
.kalender__filter {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    min-width: 0;
    max-width: 100%;
}

.kalender__filter .field__input {
    max-width: 100%;
}

.kalender__ansicht {
    display: flex;
    gap: 4px;
}

/* Week: seven columns from tablet up, stacked below that — seven columns
   at 390 px would be 50 px wide each and no title would stay readable. */
.woche {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
}

.woche__tag {
    min-height: 240px;
    min-width: 0;
    padding: 6px;
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.woche__tag:last-child {
    border-right: 0;
}

.woche__kopf {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 4px;
    min-width: 0;
    margin-bottom: 2px;
    color: var(--text-muted);
    font-size: 13px;
    font-weight: 600;
    text-decoration: none;
}

.woche__leer {
    margin: 0;
    color: var(--text-dim);
    font-size: 12px;
}

/* Task in the calendar sheet: dashed edge instead of appointment color —
   a deadline is not an appointment, you don't drive there at a set time.
   Double class because `.cal__event` is defined further down and would
   overwrite the edge again via the `border-left` shorthand. */
.cal__event.cal__event--aufgabe {
    border-left-style: dashed;
    border-left-color: var(--text-dim);
}

.cal__event--erledigt .cal__eventtitle {
    color: var(--text-dim);
    text-decoration: line-through;
}

.cal__eventwer {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-muted);
    font-size: 12px;
}

.cal__daylink {
    color: inherit;
    text-decoration: none;
}

.cal__more {
    color: var(--text-muted);
    font-size: 12px;
    text-decoration: none;
}

@media (max-width: 860px) {
    .woche {
        grid-template-columns: minmax(0, 1fr);
    }

    .woche__tag {
        min-height: 0;
        border-right: 0;
        border-bottom: 1px solid var(--border);
    }

    .woche__tag:last-child {
        border-bottom: 0;
    }
}

/* --------------------------------------------------------------------------
   Feedback button and short message — both on EVERY page (Feedback #12933)
   -------------------------------------------------------------------------- */

/* Help and feedback, round icons STACKED in the bottom right corner: the
   column is 36px wide, and `.content` keeps it free with its side padding,
   so the buttons never cover a link at any scroll position. `z-index` BELOW
   the modal (60) and the expanded sidebar (65/70): they open dialogs. */
.fabs {
    position: fixed;
    right: 24px;
    bottom: 24px;
    z-index: 50;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.fab {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 10px;
    border: 0;
    border-radius: 9999px;
    background: var(--accent);
    color: #fff;
    cursor: pointer;
    /* Just a neutral shadow, no more colored glow — it made the button stand
       out more than the page it sits on. */
    box-shadow: 0 2px 6px rgba(17, 28, 39, 0.15);
}

.fab:hover {
    background: var(--accent-bright);
}

.fab svg {
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
}

/* The help dialog: the manual chapter of the page (`base.html`). */
.hilfe p {
    margin: 0 0 12px;
    line-height: 1.5;
}

.hilfe__kapitel {
    margin: 24px 0 8px;
    padding-top: 16px;
    border-top: 1px solid var(--border);
    font-size: 16px;
    font-weight: 600;
}

.hilfe__titel {
    margin: 16px 0 6px;
    font-size: 14px;
    font-weight: 600;
}

.hilfe__liste {
    margin: 0 0 12px;
    padding-left: 20px;
    line-height: 1.5;
}

.hilfe__liste li + li {
    margin-top: 6px;
}

/* Confirmation for pages without their own message line — above the
   button so it doesn't cover it. */
.toast {
    position: fixed;
    right: 24px;
    bottom: 124px;
    z-index: 55;
    max-width: min(360px, calc(100vw - 48px));
    margin: 0;
    padding: 12px 16px;
    border-radius: var(--radius);
    background: var(--text);
    color: #fff;
    font-size: 13px;
    line-height: 20px;
    box-shadow: 0 6px 18px rgba(17, 28, 39, 0.25);
}

.toast--error {
    background: var(--signal-red);
}

/* --------------------------------------------------------------------------
   Notification modal
   -------------------------------------------------------------------------- */

.modal {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px 16px;
}

/* An open dialog locks scrolling of the page behind it. Here the document
   itself scrolls (the sidebar is `sticky`), so the lock belongs on `html`.
   It hangs on the `hidden` attribute through which EVERY dialog is
   opened — so it applies to all of them, without an opener having to set
   it or a closer being able to forget it. */
html:has(.modal:not([hidden])) {
    overflow: hidden;
}

.modal__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(17, 28, 39, 0.45);
}

.modal__box {
    position: relative;
    width: 100%;
    max-width: 460px;
    /* The full available height, not four fifths of it: `.modal` already
       pads 24 px on top and bottom, so `100%` here is `100vh - 48px`. The
       former `80vh` gave away a fifth of the screen and still cut off the
       content (Feedback #13099). */
    max-height: 100%;
    display: flex;
    flex-direction: column;
    /* The surface named ONCE, so the pinned button row at the bottom gets
       the same background — in both themes (see `.modal__buttons`). */
    --modal-flaeche: var(--surface);
    background: var(--modal-flaeche);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.22);
    overflow: hidden;
}

.modal__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 13px 12px 13px 18px;
    border-bottom: 1px solid var(--border);
    font-size: 15px;
    font-weight: 700;
}

.modal__headactions {
    display: flex;
    align-items: center;
    gap: 8px;
}

.modal__action {
    background: none;
    border: none;
    padding: 0;
    color: var(--accent-dark);
    font-family: inherit;
    font-size: 13px;
    font-weight: 400;
    cursor: pointer;
}

.modal__action:hover {
    color: var(--accent);
    text-decoration: underline;
}

.modal__box--sm {
    max-width: 400px;
}

/* A dialog that carries a TABLE instead of two input fields — the
   overview of product groups. In the 460 px of an input dialog every
   group name wrapped across three lines, and the table was 467 px, seven
   pixels wider than its box. The action buttons were still INSIDE
   (measured: right edge 872 against a box up to 950) — they were
   invisible for a different reason, see `.rowactions--fixed`. */
.modal__box--wide {
    max-width: 680px;
}

/* A dialog made of many field rows (the incoming invoice): the 4 px of a
   `.field` let a label stick to the input above it — unless that one had a
   hint below, then there was air. The same space above every label, also
   when the rows stack on the phone. */
.modal__pad--raster .field {
    margin-bottom: 14px;
}

/* Same lesson as with `.card--flush` (see there): a table in a box with
   `overflow: hidden` doesn't get narrow, it gets CUT OFF — with no way to
   scroll. On a phone the box isn't 680 px wide; `overflow-x: auto` turns
   it into a scroll area instead of cutting away the action column. */
.modal__table {
    overflow-x: auto;
}

/* A comparison table with many narrow number columns (the project-type
   catalog) should FIT the dialog, not scroll inside it. Measured, it was
   701 px wide in 642 px of space — the 16 px padding per side across six
   columns was more than the overhang. At 8 px it fits down to a 701-px
   window width; below that the `.col-sm` columns are dropped. Double
   class because the mobile block (`max-width: 860px`) further down sets
   `.table thead th` to 12 px and would otherwise win. */
.table.table--dicht thead th,
.table.table--dicht tbody td {
    padding-left: 8px;
    padding-right: 8px;
}

/* ⚠️ Scrolling happens HERE, not in the box: that one carries
   `overflow: hidden` so its rounded corners clip the head and the table.
   Without this line a dialog whose content got taller than the screen
   was not simply short — it got CUT OFF at the bottom, with no way to
   scroll. Measured in the feedback dialog (#13099): the submit button sat
   79 px below the box at an 800-px window height, 159 px at 700 px, and
   was unreachable by any gesture. Same lesson as with `.card--flush` and
   `.modal__table` — a clipping box must be scrollable. */
.modal__pad {
    padding: 18px;
    overflow-y: auto;
}

.modal__pad p {
    margin: 0;
    font-size: 14px;
    line-height: 21px;
    color: var(--text-muted);
}

/* The button row stays put while the content above it scrolls away.
   The reporter of #13099 did not report „too little space", but that
   the submit button DISAPPEARS as soon as the input field grows
   (`.field__input--area` can be dragged by its handle). Scrollability
   alone would have made it reachable, but still invisible — and you
   have to know about a button to scroll to it.

   The negative outer margins cancel the 18 px padding of `.modal__pad`,
   so the background reaches edge to edge; `bottom` mirrors them so the
   row sticks flush at the bottom. Deliberately WITHOUT a divider: in a
   short dialog nothing sticks here, and a line there would be a visible
   change to 32 dialogs that nobody asked for. */
.modal__buttons {
    position: sticky;
    bottom: -18px;
    z-index: 1;
    display: flex; flex-wrap: wrap; /* a narrow dialog wraps, never pushes a button out */
    justify-content: flex-end;
    gap: 8px;
    margin: 20px -18px -18px;
    padding: 12px 18px 18px;
    background: var(--modal-flaeche);
    -webkit-backdrop-filter: var(--glas, none);
    backdrop-filter: var(--glas, none);
}

.modal__body {
    overflow-y: auto;
}

.modal__empty {
    margin: 0;
    padding: 30px 16px;
    text-align: center;
    color: var(--text-dim);
    font-size: 13px;
}

/* „Einzelauftrag oder Projekt?" (`_auftragswahl.html`): two large
   surfaces instead of two buttons — the explanation below is the reason
   for the choice, and therefore belongs INSIDE the click target. */
.auftragswahl__frage {
    margin: 0 0 12px;
    color: var(--text-muted);
    font-size: 13px;
}

.auftragswahl {
    display: grid;
    gap: 10px;
}

.auftragswahl__weg {
    display: block;
    width: 100%;
    padding: 14px 16px;
    text-align: left;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-family: inherit;
    color: var(--text);
    cursor: pointer;
}

.auftragswahl__weg:hover {
    background: var(--surface-hover);
    border-color: var(--accent);
}

.auftragswahl__weg:focus-visible {
    outline: 2px solid var(--accent-dark);
    outline-offset: 2px;
}

.auftragswahl__titel {
    display: block;
    font-size: 15px;
    font-weight: 600;
}

.auftragswahl__text {
    display: block;
    margin-top: 4px;
    color: var(--text-dim);
    font-size: 13px;
    line-height: 19px;
}

.note {
    display: block;
    width: 100%;
    text-align: left;
    padding: 12px 18px;
    background: none;
    border: none;
    border-bottom: 1px solid var(--border);
    border-left: 3px solid transparent;
    font-family: inherit;
    cursor: pointer;
    /* An entry with a target is an `<a>` (glocke.js) — no link colour,
       no underline; it looks like the `<button>` rows. */
    color: inherit;
    text-decoration: none;
}

.note:last-child {
    border-bottom: none;
}

.note:hover {
    background: var(--surface-hover);
}

.note--warn {
    border-left-color: var(--signal-orange);
}

.note--critical {
    border-left-color: var(--signal-red);
}

.note--ok {
    border-left-color: var(--signal-green);
}

/* Unread: bold title — the color edge already carries the level. */
.note--unread .note__title {
    font-weight: 600;
    color: var(--text);
}

.note__title {
    display: block;
    font-size: 13px;
    line-height: 19px;
    color: var(--text-muted);
}

.note__message {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    line-height: 18px;
    color: var(--text-dim);
}

.note__time {
    display: block;
    margin-top: 4px;
    font-size: 11px;
    color: var(--text-dim);
}

/* --------------------------------------------------------------------------
   Pinned or unpinned (Feedback #13098)
   --------------------------------------------------------------------------
   Modeled on pvwebapp's `MiniDrawer`: pinned, the rail stays permanently
   open (the previous state); unpinned, it collapses to an icon column
   and comes back out on hover.

   ⚠️ Unpinned, the rail lies OVER the content, it does not push it.
   Hence `position: fixed` plus a `padding-left` on `.shell` that keeps
   the icon column's space free: if the rail grew in flow, the whole
   content would shift on every hover — exactly what pvwebapp's reserved
   slot prevents.

   ⚠️ Everything here applies from 861 px up. Below that the rail is an
   overlay with its own opener anyway; a second mechanism next to it would
   produce two truths about whether the menu is currently visible.
   -------------------------------------------------------------------------- */

@media (min-width: 861px) {
    html[data-nav-pin="0"] .shell {
        padding-left: var(--sidebar-rail);
    }

    html[data-nav-pin="0"] .sidebar {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 60;
        height: 100vh;
        width: var(--sidebar-rail);
        transition: width 0.18s ease, box-shadow 0.18s ease;
    }

    /* `focus-within` belongs here: whoever moves through the rail with the
       keyboard would otherwise never expand it and would tab blindly
       through icons without labels. */
    html[data-nav-pin="0"] .sidebar:hover,
    html[data-nav-pin="0"] .sidebar:focus-within {
        width: var(--sidebar-w);
        box-shadow: var(--shadow);
    }

    /* --- collapsed: icons only ------------------------------------------ */

    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .sidebar__brand {
        justify-content: center;
        padding: 16px 0;
    }

    /* The pin button itself is gone when collapsed — to re-pin, you first
       expand the rail. Same condition as in pvwebapp: next to the logo
       there is no room in 72 px, and a button that slides under the
       mouse pointer on hover is harder to hit than one that appears with
       the rail. */
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .sidebar__brand span,
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .sidebar__pin,
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .nav__text,
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .nav__label,
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .nav__chevron,
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .userbar__id {
        display: none;
    }

    /* The ENTRIES of a group stay gone when collapsed — even those of an
       open group. A row of identical-looking icons without the head
       above them is unreadable; the group itself is the entry point, and
       its entries come on hover. */
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .nav__items {
        display: none;
    }

    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .nav {
        padding: 12px 8px;
    }

    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .nav a,
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .nav__toggle {
        position: relative;
        justify-content: center;
        padding-left: 0;
        padding-right: 0;
    }

    /* ⚠️ EVERY counter turns into a dot — the one on the group head
       (`.nav__count`) AND the one on the bell (`.iconbtn__badge`). The
       number doesn't fit next to an icon: on the bell the two-digit
       counter almost completely covered the icon (measured, a 30-px
       button against a 16-px badge). That is a different bug than an
       overflow, and `e2e_layout.py` doesn't catch it — nothing falls out
       of its box, it just overlaps.

       How many there are is shown by the expanded menu. A signal without
       a number is better than a number without an icon. Whoever
       introduces a third badge belongs on this list. */
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .nav__count,
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .iconbtn__badge {
        position: absolute;
        margin: 0;
        min-width: 8px;
        width: 8px;
        height: 8px;
        padding: 0;
        font-size: 0;
        line-height: 0;
    }

    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .nav__count {
        top: 3px;
        right: 12px;
    }

    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .iconbtn__badge {
        top: 1px;
        right: 1px;
    }

    /* Four icons in 72 px: two rows of two. Stacked, the card would be
       128 px tall and push the rail into scrolling.

       ⚠️ The math works out tightly and is MEASURED in the browser, not
       estimated: of the 72 px, the 1-px border on the right and the side
       padding are subtracted. With the usual 12 px padding and 32-px
       icons the four would fall into FOUR rows (measured: 149 px tall)
       — and that WITHOUT anything overflowing, so a layout test wouldn't
       catch it. 2 px padding leaves 67 px, two 30-px icons with 4 px
       gap need 64. */
    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .userbar {
        flex-wrap: wrap;
        justify-content: center;
        padding: 8px 2px;
    }

    html[data-nav-pin="0"] .sidebar:not(:hover):not(:focus-within) .userbar .iconbtn {
        width: 30px;
        height: 30px;
    }

    /* --- expanded, the rail lies OVER the content ----------------------- */

    /* More opaque than usual: unpinned, it is no longer the page background
       showing through, but the text beside it — and the rail is no
       longer readable on that. */
    html[data-thema="glass"][data-nav-pin="0"] .sidebar:hover,
    html[data-thema="glass"][data-nav-pin="0"] .sidebar:focus-within {
        background: rgba(var(--glas-rgb), 0.88);
    }
}

/* Below 861 px the rail is an overlay with its own opener — the pin
   button would have nothing to do there. */
@media (max-width: 860px) {
    .sidebar__pin {
        display: none;
    }
}

/* --------------------------------------------------------------------------
   Narrow viewports — rail slides in as an overlay
   -------------------------------------------------------------------------- */

@media (max-width: 860px) {
    .mobilebar {
        display: flex;
    }

    .sidebar {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 70;
        height: 100dvh;
        width: 288px;   /* w-72 */
        transform: translateX(-100%);
        /* `visibility` in addition to shifting: otherwise the collapsed
           rail stays in the tab order and reaches the screen reader —
           focus then wanders into invisible controls. */
        visibility: hidden;
        transition: transform 0.2s ease-out, visibility 0.2s ease-out;
        border-right: 1px solid var(--border);
    }

    .sidebar.is-open {
        transform: translateX(0);
        visibility: visible;
    }

    .sidebar__backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 65;
        background: rgba(17, 28, 39, 0.45);
    }

    .sidebar__close {
        display: flex;
    }

    /* The distribution of free space (`.nav--section` grows, all the rest
       stays fixed) is a DESKTOP rule — it arranges gaps that don't exist
       on the phone at all. There it instead squeezed the only growing
       area („Sonstiges") down to 4 px: its entries were effectively
       gone. Above 860 px everything stays as described. */
    .nav--main,
    .nav--last {
        flex: 0 0 auto;
        overflow: visible;
    }
    /* `.nav--section` is defined further down and would win here against
       this rule — its mobile level therefore lives there. */

    /* Finger-wide targets. 38 px (entry), 24 px (group head) and 32 px
       (icon button) were all below the 44 px you can reliably hit. */
    .nav a {
        padding: 11px 12px;
        font-size: 15px;
    }

    .nav__toggle {
        padding: 12px 12px 12px 14px;
    }

    /* One step larger than on the desktop, like the entries (15 px). */
    .nav__label {
        font-size: 16px;
    }

    .userbar .iconbtn,
    .mobilebar .iconbtn {
        width: 40px;
        height: 40px;
    }

    .content {
        padding: 16px 60px 16px 16px; /* right: .fabs 16 + 36 + 8 */
    }

    .fabs {
        right: 16px;
        bottom: 16px;
    }

    .toast {
        right: 60px;
        left: 16px;
        bottom: 80px;
        max-width: none;
    }

    .pagehead h1 {
        font-size: 24px;
    }

    /* The table card's `overflow-x: auto` used to live HERE — so it only
       applied on the phone, even though a table can get too wide at any
       width. It has become the base rule of `.card--flush`
       (Feedback #13056, reasoning there). */

    /* Less padding means less to scroll. */
    :root {
        --card-inset: 16px;
    }

    .table thead th,
    .table tbody td {
        padding: 10px 12px;
    }

    /* The 190-px label column doesn't leave 150 px next to it for the
       value — on the phone the value sits below its label. */
    .datalist {
        grid-template-columns: minmax(0, 1fr);
        gap: 2px 0;
    }

    .datalist dd + dt {
        margin-top: 10px;
    }
}

/* ==========================================================================
   Administration — groups and permissions
   Permissions hang on groups (modeled on pvwebapp, `group_management/`): one
   card per group, with a toggle for each permission from the catalog.
   ========================================================================== */

/* Head of a group card: name on the left, actions on the right */
.group + .group {
    margin-top: 16px;
}

.group__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    /* ⚠️ Wrap like `.rowactions` (Feedback #13056). In a HALF card of the
       `.cardgrid` — „Verbrauchtes Material" since Feedback #13418 — a
       two-line title and a button sit side by side in about 300 px and
       almost touch. They may then stack instead of being squeezed to the
       last pixel. Nothing changes in the wide cards: both already fit in
       one line there. */
    flex-wrap: wrap;
    gap: 8px 16px;
    margin-bottom: 4px;
}

.group__head .section-title {
    margin: 0;
}

/* ------------------------------------------------ Collapsible group card
   Feedback #15320: seven roles times almost forty permissions were about
   280 toggles stacked up — the page no longer answered „which roles are
   there?". Every card therefore collapses; the state lives in the cookie
   `gruppen_auf` and is rendered server-side (see `gruppen.html`).

   The left half of the head grows, the actions on the right stay where
   they are — otherwise the title jumped depending on name length.
   `min-width: 0` keeps a long name in its box instead of pushing the buttons out. */
.group__title {
    flex: 1 1 220px;
    min-width: 0;
}

/* The heading now only carries the button — its bottom spacing comes
   from the subtitle. */
.section-title--klapp {
    margin: 0;
}

/* The whole title row is the button, not just the chevron — same
   reasoning as with `.nav__toggle` (Feedback #12818): a 14-px arrow is
   not a target you can hit. */
.group__toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin: 0;
    padding: 0;
    background: none;
    border: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.group__toggle:hover {
    color: var(--accent);
}

.group__chevron {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
    transition: transform 0.15s ease;
}

/* Collapsed, the chevron points right — like in the sidebar. */
.group__toggle[aria-expanded="false"] .group__chevron {
    transform: rotate(-90deg);
}

/* Collapsed, the subtitle is all the card still says: that's why it also
   shows how many permissions are set. Unsaved changes stand out in
   color — otherwise a collapsed card would silently hide a change in
   progress. */
.group__dirty {
    color: var(--signal-orange);
    font-weight: 600;
    /* The separating dot belongs to the word: without this it stuck at the
       end of the line and „ungespeichert" slid alone into the next line. */
    white-space: nowrap;
}

.group__body[hidden] {
    display: none;
}

/* ⚠️ A section head in a BORDERLESS card needs its own padding.
   `.card--flush` deliberately has none — the table inside it should run
   edge to edge. A `.group__head` thus sat with no spacing at all in the
   16-px rounded corner: title and button read as if they stood OUTSIDE
   the box (the same confusion as in Feedback #13021), and the rounding
   clipped the heading („Termine", „Aufgaben", „Rechnungen") at the top.
   16 spots across 9 templates were affected, four times on the project
   file alone. The head therefore brings its own padding instead of giving
   it to the card (which would have to take it away from the table again).

   `--card-inset` applies on the sides, including the first and last
   table column (rule below): this puts the heading exactly above the
   first column (Feedback #13056) AND flush with the title of a padded
   card above or below it — previously 16 against 26 px.

   `position: sticky` keeps it in place while scrolling sideways: since
   #13056 the card is a scroll area (see `.card--flush`), and a heading
   that runs out of view to the right while scrolling would be exactly
   the restlessness the feedback was meant to fix. */
.card--flush > .group__head {
    position: sticky;
    left: 0;
    padding: 20px var(--card-inset) 14px;
    margin-bottom: 0;
}

.card--flush .table :is(th, td):first-child {
    padding-left: var(--card-inset);
}

.card--flush .table :is(th, td):last-child {
    padding-right: var(--card-inset);
}

/* The spacing already sits in the head's padding — both together would
   tear open a gap between button and table. */
.card--flush > .group__head .rowactions--head {
    margin-bottom: 0;
}

/* Same reasoning at the other end: a footnote BELOW the table of a
   borderless card likewise needs its own padding, otherwise it sits in
   the rounded corner and gets clipped (Feedback #13056). On the sides
   like the head, so it starts below the first column. */
.card--flush > .cardnote {
    padding: 12px var(--card-inset) 16px;
    margin: 0;
}

/* Actions above or below a card — unlike `.rowactions` in a table row,
   permanently visible, they don't depend on a hover. The 2 px of a table
   row are too tight here: next to a button there is often a TEXT (the
   month total on /loehne), which then glued onto the button's frame —
   and without `center` it sat higher than the button's label. */
.rowactions--head {
    justify-content: flex-end;
    align-items: center;
    gap: 12px;
    margin-bottom: 16px;
    opacity: 1;
}

/* ⚠️ Always visible, not only on hover. `.rowactions` deliberately
   fades out on the desktop (`opacity: 0`) — that fits for row ACTIONS at
   the edge, not for a cell's CONTENT. The appointment suggestions on
   /wartungen are buttons, but they are also the column's information:
   invisible until hovered, the column would seem empty, and the page
   would no longer answer its own question. Left-aligned for the same
   reason — they sit in the column, not at the end of the row. */
.rowactions--immer {
    opacity: 1;
    justify-content: flex-start;
}

/* The proof of work (Arbeitsnachweis, Feedback #18594): the sheet the
   customer signs with a finger on the installer's phone. `touch-action:
   none` is the core of it — without it the finger scrolls the page
   instead of drawing. The field is white in EVERY theme: the signature is
   drawn black and saved as a PNG, exactly how it ends up in the PDF. */
.arbeitsnachweis__titel {
    margin-top: 18px;
}

/* Hours and quantities don't wrap: „0,75 / Std." on two lines reads on
   the phone like two separate values. */
.arbeitsnachweis__zahl {
    white-space: nowrap;
    text-align: right;
}

.arbeitsnachweis__feld {
    display: block;
    width: 100%;
    height: 180px;
    margin: 10px 0;
    background: #ffffff;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
    touch-action: none;
    cursor: crosshair;
}

.arbeitsnachweis__bild {
    display: block;
    max-width: 100%;
    height: auto;
    max-height: 140px;
    margin: 10px 0;
    background: #ffffff;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* `flex-end` instead of the inherited stretch: here an input field WITH
   a label („Verlaufseintrag") sits next to the button, and stretched the
   button became as tall as label plus field together — it then stood
   20 px above the field it relates to (Feedback #13056). Bottom-aligned,
   both sit on one line. */
.rowactions--foot {
    align-items: flex-end;
    justify-content: flex-end;
    margin-top: 16px;
    opacity: 1;
}

/* List of toggles — same shape for a group's permissions and for a
   user's group selection. */
.perms {
    margin: 12px 0 0;
    padding: 0;
    list-style: none;
}

.perms__item + .perms__item {
    border-top: 1px solid var(--border);
}

.perms__row {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 11px 2px;
    cursor: pointer;
}

.perms__row input[type='checkbox'] {
    flex: 0 0 auto;
    width: 17px;
    height: 17px;
    margin: 1px 0 0;
    accent-color: var(--accent);
    cursor: pointer;
}

/* Permanently assigned permissions of a system group: visible but not operable */
.perms__row input[type='checkbox']:disabled,
.perms__row input[type='checkbox']:disabled ~ span {
    cursor: default;
    opacity: 0.65;
}

.perms__label {
    display: block;
    font-size: 14px;
    font-weight: 500;
}

.perms__desc {
    display: block;
    margin-top: 1px;
    color: var(--text-dim);
    font-size: 12px;
    line-height: 18px;
}

/* Group badges in the user table */
.grouptags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

.badge--group {
    background: var(--surface-hover);
    color: var(--text-muted);
}

/* Single input field in a dialog (group name) */
.field {
    display: block;
    margin-bottom: 4px;
}

.field__label {
    display: block;
    margin-bottom: 6px;
    font-size: 13px;
    font-weight: 500;
}

.field__input {
    width: 100%;
    padding: 9px 11px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: 14px;
}

/* A select ignores the inherited 22-px line height and came out 4 px lower
   than a text field — next to each other (search row) the labels above
   them stood at different heights. 42 = 22 line + 2 x 9 padding + 2 border. */
select.field__input:not([multiple]) {
    min-height: 42px;
}

.field__input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

/* iOS Safari zooms into any field below 16 px on tap and leaves the page
   shifted afterwards (Feedback #12851). The rule lives HERE and not in
   the mobile block further up: there it would come before the
   definition above and lose against it. */
@media (max-width: 860px) {
    .field__input {
        font-size: 16px;
    }
}

/* Selection with a search field (Feedback #18144, `js/app/auswahlsuche.js`).
   On a PAGE the result list floats below the field, over the content —
   in flow it stretched the whole filter bar (Zeiterfassung, Kalender).
   In a DIALOG it sits in flow: `.modal__pad` scrolls, and a floating list
   would get clipped at its edge — exactly where the field sits far down.
   Long names wrap instead of widening the box. */
.auswahlsuche {
    position: relative;
}

.auswahlsuche__zeile {
    position: relative;
}

.auswahlsuche__feld {
    padding-right: 38px;
}

.auswahlsuche__leeren {
    position: absolute;
    top: 50%;
    right: 5px;
    width: 28px;
    height: 28px;
    transform: translateY(-50%);
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    color: var(--text-dim);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
}

.auswahlsuche__leeren:hover {
    background: var(--surface-hover);
    color: var(--text);
}

.auswahlsuche__liste {
    max-height: 280px;
    margin: 4px 0 0;
    padding: 4px;
    overflow-y: auto;
    list-style: none;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow);
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 10;
}

.modal .auswahlsuche__liste {
    position: static;
}

.auswahlsuche__eintrag {
    padding: 6px 8px;
    border-radius: 6px;
    cursor: pointer;
    overflow-wrap: anywhere;
}

.auswahlsuche__eintrag.is-aktiv {
    background: var(--accent-soft);
}

.auswahlsuche__eintrag[aria-selected="true"] .auswahlsuche__name {
    font-weight: 600;
}

.auswahlsuche__name {
    display: block;
    font-size: 14px;
}

.auswahlsuche__zusatz {
    display: block;
    color: var(--text-muted);
    font-size: 12px;
}

.auswahlsuche__nichts {
    padding: 6px 8px;
    color: var(--text-muted);
    font-size: 13px;
}

.auswahlsuche__mehr {
    margin: 4px 2px 0;
    color: var(--text-muted);
    font-size: 12px;
}

/* Multi-select: each choice as a chip below the field */
.auswahlsuche__marken {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 6px 0 0;
    padding: 0;
    list-style: none;
}

.auswahlsuche__marke {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    max-width: 100%;
    padding: 2px 2px 2px 10px;
    border-radius: 9999px;
    background: var(--accent-soft);
    color: var(--accent-dark);
    font-size: 13px;
    overflow-wrap: anywhere;
}

.auswahlsuche__weg {
    flex: none;
    width: 24px;
    height: 24px;
    border: 0;
    border-radius: 9999px;
    background: transparent;
    color: inherit;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
}

.auswahlsuche__weg:hover {
    background: var(--surface-hover);
}

/* Error INSIDE the dialog — it stays open so the input isn't lost */
.field__error {
    margin: 8px 0 0;
    color: var(--signal-red);
    font-size: 13px;
    line-height: 19px;
}

/* ==========================================================================
   Feedback — own reports and their status
   ========================================================================== */

.fb {
    padding: 16px 20px;
}

.fb + .fb {
    border-top: 1px solid var(--border);
}

.fb__head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 8px;
}

.fb__meta {
    color: var(--text-dim);
    font-size: 12px;
}

.fb__message {
    margin: 0;
    font-size: 14px;
    line-height: 21px;
    white-space: pre-wrap;
}

/* The developers' answer is the reason someone checks here — hence set
   apart with a blue edge instead of as another paragraph. */
.fb__answer {
    margin: 10px 0 0;
    padding: 10px 14px;
    border-left: 3px solid var(--accent);
    background: var(--accent-soft);
    border-radius: 0 var(--radius) var(--radius) 0;
    font-size: 14px;
    line-height: 21px;
    white-space: pre-wrap;
}

.fb__change,
.fb__answered {
    margin: 8px 0 0;
    color: var(--text-dim);
    font-size: 12px;
    line-height: 19px;
}

/* An open question blocks the feedback — it must stand out. */
.fb__clarify {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 14px;
    margin-top: 10px;
    padding: 12px 14px;
    border: 1px solid var(--signal-orange);
    border-radius: var(--radius);
    background: rgba(232, 133, 10, 0.07);
}

.fb__question {
    margin: 0;
    font-size: 13px;
    line-height: 20px;
}

/* In the feedback dialog the question sits above the reply field and
   needs room — in the list it sits in its own box and gets none. */
[data-feedback-nachfrage] .fb__question {
    margin-bottom: 14px;
    font-weight: 600;
}

.badge--done {
    background: rgba(47, 143, 87, 0.12);
    color: var(--signal-green);
}

/* ⚠️ `width: auto` belongs to this (Feedback #13056). `.btn` is
   `width: 100%` by default — meant for the one button below a form.
   `--sm` however only exists in button rows (the `flex: 0 0 auto` already
   says so), and there a `.btn.btn--sm` without `--auto`/`--secondary`
   claimed the full row width for itself alone: on the customer file
   „Projekt anlegen" pushed the three buttons beside it out of view
   (439-px row at a 390-px viewport). Of 63 occurrences exactly ONE was not
   already saved by `--auto` or `--secondary` — the default was wrong. */
.btn--sm {
    flex: 0 0 auto;
    width: auto;
    padding: 5px 12px;
    font-size: 13px;
}

/* Multi-line field in the dialog (feedback text, question reply) */
.field__input--area {
    resize: vertical;
    min-height: 96px;
    line-height: 21px;
}

/* The question in the reply dialog — quote of what is being answered */
.clarify__question {
    margin: 0 0 16px;
    padding: 10px 14px;
    border-left: 3px solid var(--signal-orange);
    background: rgba(232, 133, 10, 0.07);
    border-radius: 0 var(--radius) var(--radius) 0;
    font-size: 14px;
    line-height: 21px;
}

@media (max-width: 560px) {
    .fb__clarify {
        flex-direction: column;
    }
}


/* ===========================================================================
   Organization — calendar, time tracking, inventory, tasks, personnel

   Everything here builds on the blocks above (card, table, badge,
   modal). The only thing new is what the five pages really need: a
   month grid, metric tiles, a filter row and tabs.
   =========================================================================== */

/* A zone of the sidebar (Verkauf, Finanzen, Projekte, Logistik,
   Verwaltung, Informationen, Sonstiges). It sits below the main
   navigation and may grow along — it is the work area, not the annex.

   ⚠️ Deliberately WITHOUT its own `overflow-y: auto` (Feedback #12851).
   A scroll area of its own sets `min-height` to 0 — the group could then
   shrink below its content and, as soon as the rail no longer fit, got
   squeezed down to 4 px as the only yielding one: its entries were there,
   but unreachable in a 4-px-tall window. Without the rule,
   `min-height: auto` takes effect again, the group keeps its height, and
   the overflow lands on the rail, which scrolls it. It may still grow
   (`flex: 1 1 auto`) — on a tall screen it takes the free space as before. */
.nav--section {
    flex: 1 1 auto;
    padding: 0 12px 4px;
    /* Resets the `overflow-y: auto` of the base class `.nav` — as
       `.nav--main` and `.nav--section--fixed` next to it also do. Just
       omitting it is NOT enough here: inherited, it would stay in effect. */
    overflow: visible;
}

/* Of the seven zones only the BOTTOM one („Sonstiges") grows. If
   several grew, the free space would split between them and the gaps
   would sit in the middle of the groups instead of below them. Keeping
   feedback and the user card together at the bottom is carried by the
   growing group above them. */
.nav--section--fixed {
    flex: 0 0 auto;
    overflow: visible;
}

/* Both only apply on the DESKTOP. They arrange free space — and there is
   none of that on a phone: there the rule squeezed the only growing
   area („Sonstiges") down to 4 px, its entries were effectively gone,
   and because `flex-shrink: 0` hangs on all the others, the rail ran out
   of view at the bottom — taking the user card with it (Feedback
   #12851). On narrow viewports the whole rail scrolls as one column
   instead (`.sidebar { overflow-y: auto }` in the mobile block further up).

   The breakpoint lives HERE instead of in that block: there it would come
   BEFORE these two definitions and lose against them at equal specificity. */
@media (max-width: 860px) {
    .nav--section,
    .nav--section--fixed {
        flex: 0 0 auto;
        overflow: visible;
    }
}

/* --- Calendar month bar -------------------------------------------------- */

.monthbar {
    display: flex;
    align-items: center;
    /* ⚠️ Wraps, like `.rowactions` since #13056: four elements (back,
       month name, forward, today) don't fit in one row on a phone —
       measured, „Heute" stuck out 29 px past the screen at 390 px, and
       the whole page scrolled sideways. */
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: 16px;
}

.monthbar__title {
    font-size: 16px;
}

.monthbar__today {
    margin-left: auto;
}

/* --- Month grid ------------------------------------------------------------ */

.cal {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
}

.cal__weekday {
    padding: 10px 12px;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 11px;
    font-weight: 600;
    text-transform: none;
}

.cal__day {
    min-height: 140px;
    padding: 6px;
    border-right: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.cal__day:nth-child(7n + 7) {
    border-right: 0;
}

/* Days of neighboring months: visible, but toned down — a week with
   gaps cannot be read. */
.cal__day--out {
    background: var(--bg);
}

.cal__day--out .cal__daynum {
    color: var(--text-dim);
}

.cal__day--today {
    box-shadow: inset 0 0 0 2px var(--accent);
}

.cal__day--holiday {
    background: var(--accent-soft);
}

.cal__daynum {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 4px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-muted);
}

.cal__holiday {
    font-size: 11px;
    font-weight: 500;
    color: var(--accent-dark);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cal__event {
    display: block;
    width: 100%;
    text-align: left;
    padding: 3px 6px;
    border: 0;
    border-left: 3px solid var(--accent);
    border-radius: 4px;
    background: var(--surface-hover);
    color: var(--text);
    font: inherit;
    font-size: 13px;
    line-height: 1.35;
    cursor: pointer;
    /* In the project calendar the entry is a LINK into the file instead of
       a button (an appointment is changed where it is managed) — without
       this it would carry the underline of a link. */
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cal__event:hover {
    background: var(--border);
}

.cal__event--wartung { border-left-color: var(--signal-orange); }
.cal__event--termin { border-left-color: var(--signal-green); }
.cal__event--sonstiges { border-left-color: var(--border-strong); }

/* Assignment types (`Appointment.JOB_TYPES`) — the colours the company
   chose; they beat the kind because `color_key` puts the type first. */
.cal__event--angebot { border-left-color: var(--signal-green); }
.cal__event--besprechung { border-left-color: var(--text-dim); }
.cal__event--kundendienst { border-left-color: var(--signal-blue); }
.cal__event--notdienst { border-left-color: var(--signal-red); }
.cal__event--reklamation { border-left-color: var(--signal-orange); }

.cal__eventtime {
    display: block;
    color: var(--text-dim);
    font-size: 12px;
}

/* The title truncates ITSELF — `text-overflow` on the button didn't
   reach it as long as it stood as bare text next to the time block (see
   the template). An appointment thus broke off mid-word without it
   being visible; the full label still lives in the button's `title`. */
.cal__eventtitle {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cal__absent {
    margin: auto 0 0;
    color: var(--text-dim);
    font-size: 12px;
}

/* --- Metric tiles ----------------------------------------------------------- */

.statgrid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
    margin-bottom: 16px;
}

/* No tile alone in the last row: four tiles go 4 | 2 | 1 per row, six go
   3 | 2 | 1 (measured: „Stunden" alone under five on the project page, the
   fourth tile alone under three at 1024 px). The track width switches with
   `(threshold - 100%) * 999`, which is huge below the threshold and <= 0
   above it; the -1px keeps rounding from dropping a column. */
.statgrid:has(> :nth-child(4):last-child) {
    grid-template-columns: repeat(auto-fit, minmax(max(
        calc((100% - 48px) / 4 - 1px),
        min(calc((100% - 16px) / 2 - 1px), calc((848px - 100%) * 999)),
        min(100%, calc((416px - 100%) * 999))), 1fr));
}

.statgrid:has(> :nth-child(6):last-child) {
    grid-template-columns: repeat(auto-fit, minmax(max(
        calc((100% - 32px) / 3 - 1px),
        min(calc((100% - 16px) / 2 - 1px), calc((632px - 100%) * 999)),
        min(100%, calc((416px - 100%) * 999))), 1fr));
}

.stat__label {
    margin: 0;
    color: var(--text-dim);
    font-size: 12px;
    font-weight: 600;
}

.stat__value {
    margin: 4px 0 2px;
    font-size: 24px;
    font-weight: 700;
}

.stat__hint {
    margin: 0;
    color: var(--text-muted);
    font-size: 12px;
}

/* Only when a number calls for action — otherwise the color would be decoration. */
.stat--warn .stat__value {
    color: var(--signal-red);
}

/* --- Dashboard ------------------------------------------------------------ */

/* Tiles in a grid; a wide tile takes two columns, as long as there are
   two. `dense` fills the gap a wide tile leaves at the end of a row
   with the next narrow one. */
.dash {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    grid-auto-flow: row dense;
    gap: 16px;
}

.dash__tile {
    min-width: 0;
}

@media (min-width: 700px) {
    .dash__tile--wide {
        grid-column: span 2;
    }
}

.dash__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}

.dash__title {
    margin: 0;
    font-size: 16px;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dash__link {
    flex: 0 0 auto;
    font-size: 13px;
    color: var(--accent-dark);
}

.dash__stats {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 24px;
}

.dash__rows {
    list-style: none;
    margin: 14px 0 0;
    padding: 0;
    border-top: 1px solid var(--border);
}

.dash__row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 7px 0;
    border-bottom: 1px solid var(--border);
    font-size: 13px;
}

.dash__rowtext {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dash__rowside {
    flex: 0 0 auto;
    color: var(--text-muted);
}

.dash__row--warn .dash__rowside {
    color: var(--signal-red);
}

.dash__empty {
    margin: 14px 0 0;
    color: var(--text-muted);
    font-size: 13px;
}

/* The „Anpassen" (customize) dialog */
.dash-pick {
    list-style: none;
    margin: 0;
    padding: 0;
}

.dash-pick__row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 0;
    border-bottom: 1px solid var(--border);
}

.dash-pick__main {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    flex: 1 1 auto;
    min-width: 0;
    cursor: pointer;
}

.dash-pick__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    font-size: 14px;
}

.dash-pick__desc {
    color: var(--text-muted);
    font-size: 12px;
}

.dash-pick__wide {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: 0 0 auto;
    color: var(--text-muted);
    font-size: 12px;
}

.dash-pick__move {
    display: flex;
    gap: 4px;
    flex: 0 0 auto;
}

/* --- Filter and tab row ------------------------------------------------------ */

.filterbar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 12px;
    margin-bottom: 16px;
}

.field--inline {
    margin: 0;
    min-width: 160px;
}

.field--grow {
    flex: 1;
}

.field--check {
    flex-direction: row;
    align-items: center;
    gap: 8px;
    min-width: 0;
    padding-bottom: 8px;
    font-size: 14px;
}

.field-row {
    display: flex;
    gap: 12px;
}

.field-row .field {
    flex: 1;
}

.tabbar {
    display: flex;
    /* Wrap instead of cutting off: the invoice list has five tabs, and the
       row otherwise doesn't fit on a phone — the last one („Alle") sat
       outside the card. With three tabs the rule changes nothing. */
    flex-wrap: wrap;
    gap: 4px;
    margin-bottom: 16px;
    padding: 6px;
}

.tabbar__tab {
    padding: 7px 14px;
    border-radius: var(--radius);
    color: var(--text-muted);
    font-size: 14px;
}

.tabbar__tab:hover {
    background: var(--surface-hover);
    color: var(--text);
}

.tabbar__tab.is-active {
    background: var(--accent-soft);
    color: var(--accent-dark);
    font-weight: 600;
}

/* --- Notice card with action required --------------------------------------- */

.card--warn {
    border-left: 3px solid var(--signal-orange);
    margin-bottom: 16px;
}

/* Explanation instead of action required: the blue edge is the app's
   accent, the orange one stays reserved for where someone must act. */
.card--info {
    border-left: 3px solid var(--accent);
    margin-bottom: 16px;
}

.lowlist {
    margin: 8px 0 0;
    padding-left: 18px;
    color: var(--text-muted);
    font-size: 14px;
}

.lowlist li + li {
    margin-top: 4px;
}

/* --- Badges of the five pages ------------------------------------------------- */

/* Appointment types */
.badge--einsatz { background: var(--accent-soft); color: var(--accent-dark); }
.badge--wartung { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text); }
.badge--termin { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--sonstiges { background: var(--border); color: var(--text-muted); }
.badge--angebot { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--besprechung { background: var(--border); color: var(--text-muted); }
.badge--kundendienst { background: rgba(31, 111, 184, 0.12); color: var(--signal-blue-text); }
.badge--notdienst { background: rgba(193, 57, 27, 0.10); color: var(--signal-red-text); }
.badge--reklamation { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text); }

/* Time entry types. Working time is the normal case and stays neutral —
   color only carries what deviates from the normal case. */
.badge--type0 { background: var(--border); color: var(--text-muted); }
.badge--type1 { background: var(--accent-soft); color: var(--accent-dark); }
.badge--type2 { background: rgba(193, 57, 27, 0.10); color: var(--signal-red-text); }
.badge--type4,
.badge--type5 { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text); }
.badge--type6,
.badge--type7 { background: var(--border); color: var(--text-muted); }

.badge--pending { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text); }
.badge--system { background: var(--border); color: var(--text-dim); }

/* Channels in the customer history (Feedback #13425).
   ⚠️ What is distinguished is the DIRECTION, not the medium: incoming
   carries the accent (someone wants something from us — that's the row
   you're looking for), outgoing stays neutral. Coloring a call and an
   email of the same direction alike is intentional — which of the two
   it was is in the text next to it, and four colors in one row no
   longer sort anything. Messenger follows the same scheme, the
   directionless „Persönlich" (#15392) sits with the accent: like an
   incoming contact it allows a callback. The class comes from
   `channel|replace('_', '-')`; `notiz` gets no badge at all (hidden by the template), otherwise every row would be marked. */
.badge--anruf-ein,
.badge--mail-ein,
.badge--messenger-ein,
.badge--persoenlich { background: var(--accent-soft); color: var(--accent-dark); }
.badge--anruf-aus,
.badge--mail-aus,
.badge--messenger-aus { background: var(--border); color: var(--text-muted); }

/* Task priority. „Normal" deliberately gets no badge (the template
   hides it): if everything were marked, nothing would mean anything. */
.badge--prio1 { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text); }
.badge--prio2 { background: rgba(193, 57, 27, 0.10); color: var(--signal-red-text); }

/* Areas of the maintenance rotation (Feedback #13143).
   ⚠️ DELIBERATELY all four the same and neutral. The obvious solution —
   one color per compass direction — would be wrong here twice over: red,
   orange and green are already taken in this app („overdue", „due
   soon", „done"), so a red „Sued" (south) would read as a warning; and
   since the freely choosable accent color (#13114), ANY new color could
   end up under one of the eight accents. A compass direction is not a
   judgment anyway — the TEXT carries the statement, not the hue.
   „Nahbereich" (nearby) is lighter because it is the opposite of an
   assignment: it fits any tour day. */
.badge--gebiet-nord,
.badge--gebiet-ost,
.badge--gebiet-sued,
.badge--gebiet-west { background: var(--border); color: var(--text-muted); }
.badge--gebiet-nahbereich { background: var(--border); color: var(--text-dim); }

/* An appointment suggestion OUTSIDE the area rotation. Dashed instead of
   colored, for the same reason as above — and it stays fully operable:
   the rotation sorts, it doesn't forbid. */
.btn--fremd {
    border-style: dashed;
}

.badge--status-offen { background: var(--border); color: var(--text-muted); }
.badge--status-laeuft { background: var(--accent-soft); color: var(--accent-dark); }
.badge--status-erledigt { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }

/* Task TYPE (Feedback #13136). Same color values as the appointment
   types — the same signal shouldn't look different twice.
   „Sonstiges" gets none: it's the default value, the template hides it
   (like „Normal" with priority). */
.badge--art-wartung { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text); }
.badge--art-stoerung { background: rgba(193, 57, 27, 0.10); color: var(--signal-red-text); }
.badge--art-reparatur { background: var(--accent-soft); color: var(--accent-dark); }
.badge--art-einbau { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--art-aufmass { background: var(--border); color: var(--text-muted); }

/* --- Photos on a task (Feedback #13136) -------------------------------------- */

/* The file field itself is invisible; the surface above it is visible.
   A native `input[type=file]` looks different on every platform and
   cannot be styled — it would remain the only foreign element in an
   otherwise fully designed interface. */
.filepick input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    /* NOT `display: none` — then the field is no longer reachable by
       keyboard and the focus ring below would never appear. */
}

.filepick {
    display: block;
    position: relative;
    cursor: pointer;
}

.filepick__face {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    /* 44 px tall: that's the area a thumb reliably hits. Anything smaller
       is a target on a phone, not a button. */
    min-height: 44px;
    padding: 10px 14px;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
    /* ⚠️ `--surface`, NOT `--flaeche`: the latter is a gradient IMAGE for
       the whole page in the glass theme and would result in a color
       blotch inside a 44-px box. */
    background: var(--surface);
    color: var(--text-dim);
    font-size: 14px;
}

.filepick:hover .filepick__face,
.filepick input[type="file"]:focus + .filepick__face {
    border-color: var(--accent);
    color: var(--accent-dark);
}

.filepick__face svg {
    width: 18px;
    height: 18px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
}

/* Hint directly below a field — without a divider and without the large
   spacing `.hint` needs below a card. */
.hint--tight {
    border-top: none;
    padding-top: 0;
    margin: 6px 0 0;
}

/* The checklist of a task (Feedback #13418). It sits in the dialog
   between the key data and the gallery: it says what needs to be done,
   the photos prove that it was done.

   Deliberately WITHOUT its own card surface — it already sits in
   `.modal__pad`, and a box inside a box looks like a bug on a phone. */
.checkliste {
    margin-top: 18px;
}

.checkliste__zeile {
    display: flex;
    align-items: baseline;
    /* ⚠️ Wrap like `.rowactions` (Feedback #13056): an item's text is up
       to 200 characters long, with two buttons next to it. Without
       `wrap` a long item would push „Foto" and „Entfernen" out of the
       dialog. */
    flex-wrap: wrap;
    gap: 4px 10px;
    padding: 7px 0;
    border-bottom: 1px solid var(--border);
}

.checkliste__haken {
    display: flex;
    align-items: baseline;
    gap: 9px;
    /* The text takes the space, the buttons get the rest. */
    flex: 1 1 60%;
    cursor: pointer;
}

.checkliste__haken input {
    /* Larger than the browser default: checked off with a thumb on a
       construction site, not with a mouse at a desk. */
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    accent-color: var(--accent);
    cursor: pointer;
}

/* Checked items are struck through and paler — visible at a glance what
   is still open, without shifting the row. */
.checkliste__zeile.is-done .checkliste__haken span {
    color: var(--text-dim);
    text-decoration: line-through;
}

.checkliste__knopf {
    flex-shrink: 0;
    font-size: 13px;
}

/* The proofs for EXACTLY this item — small, right below it, across the
   full row width (`flex-basis: 100%` wraps them below the text). */
.checkliste__belege {
    display: flex;
    flex-wrap: wrap;
    flex-basis: 100%;
    gap: 6px;
    margin-top: 2px;
}

.checkliste__belege img {
    display: block;
    width: 56px;
    height: 56px;
    border-radius: var(--radius);
    object-fit: cover;
}

.checkliste__neu {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
}

.checkliste__neu .field__input {
    flex: 1 1 180px;
}

/* The gallery. `auto-fill` instead of fixed columns: on the phone two
   images sit side by side, on the screen four — without a second rule
   per width. */
.fotos {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    gap: 10px;
    margin-top: 16px;
}

/* Tighter for the photos of a unit in the customer file: they sit IN a
   table cell, and the 120-px grid would stretch the column until the
   rest of the row was no longer readable. */
.fotos--mini {
    grid-template-columns: repeat(auto-fill, minmax(64px, 72px));
    gap: 6px;
    margin-top: 8px;
}

.fotos__item {
    position: relative;
    margin: 0;
}

.fotos__blatt {
    display: block;
    /* In the task view this is an `<a>` (image at full size), in the
       project file's construction documentation a `<button>` (leads to
       the task) — Feedback #13418. A button brings its own border,
       padding and width; without these three lines the image would sit
       in a gray box and be narrower than its column. */
    width: 100%;
    padding: 0;
    border: none;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg);
    cursor: pointer;
}

.fotos__blatt img {
    display: block;
    width: 100%;
    /* Cropped square: portrait and landscape side by side would otherwise
       form a staircase instead of a row. Whoever taps it sees the whole
       image — the sheet link opens it at full size. */
    aspect-ratio: 1 / 1;
    object-fit: cover;
}

.fotos__weg {
    position: absolute;
    top: 6px;
    right: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: 9999px;
    background: rgba(33, 33, 33, 0.62);
    color: #fff;
    cursor: pointer;
}

.fotos__weg svg {
    width: 15px;
    height: 15px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.2;
}

.fotos__item figcaption {
    margin-top: 4px;
    color: var(--text-dim);
    font-size: 12px;
    line-height: 16px;
    /* A long caption must not stretch the column — otherwise ONE image
       would shift the entire grid. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --- Files on the order: preview and label (Feedback #19233) -------------
   A row of the „Dateien" card and an entry in the „Dateien beschriften"
   dialog share the same structure: a small preview on the left, the text
   on the right. The preview is fixed at 48 px — it should make the image
   recognizable, not stretch the row. */
.datei {
    display: flex;
    align-items: center;
    gap: 12px;
}

.datei__vorschau {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg);
}

.datei__vorschau img {
    display: block;
    width: 100%;
    height: 100%;
    /* Cropped square like the gallery (`.fotos__blatt img`). */
    object-fit: cover;
}

.datei__pdf {
    color: var(--text-dim);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
}

.datei__text {
    flex: 1;
    /* A long label wraps at word boundaries. ⚠️ NOT
       `overflow-wrap: anywhere` and not `min-width: 0`: both lower the
       table column's minimum width to one character, and on the phone
       the label then stood letter by letter stacked up. Wider than the
       screen, the card scrolls horizontally as before. */
    min-width: 10em;
    overflow-wrap: break-word;
}

.datei__text.field {
    margin: 0;
}

/* In the dialog: one entry per chosen file, stacked. */
.datei-liste {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin: 14px 0;
}

/* --- Attachments in the feedback dialog -----------------------------------
   The tiles are the same as for the photos of a unit (`.fotos--mini`);
   only what the dialog needs beyond that is listed here. */
.fb-anhang__knoepfe {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.fb-anhang__hinweis {
    margin: 8px 0 0;
}

/* A PDF has no preview image — a tile of the same size, so the grid
   doesn't jump. */
.fb-anhang__datei {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1 / 1;
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 700;
}

.fb-anhang--laeuft {
    opacity: 0.45;
}

/* The cross here is text instead of SVG — the box is built in the script. */
.fb-anhang__weg {
    top: 3px;
    right: 3px;
    width: 22px;
    height: 22px;
    font-size: 16px;
    line-height: 1;
}

/* The canvas for marking. Width AND height limited: a portrait
   screenshot from the phone would otherwise be taller than the dialog,
   and the frame could no longer be dragged at the bottom. `touch-action:
   none`, otherwise the finger scrolls the page instead of dragging it. */
.fb-markieren__leinwand {
    display: block;
    max-width: 100%;
    max-height: 62vh;
    margin: 0 auto;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    cursor: crosshair;
    touch-action: none;
}

.fb-markieren__hinweis {
    margin: 0 0 10px;
}

/* --- Select an element (Feedback #19193) ----------------------------------
   The chosen element in the dialog: a row instead of a tile, it's not an
   image after all. Below it the frame and bar of the selection itself —
   above everything (the dialog sits at 60). Both let every pointer through,
   otherwise they would lie under it and be the only selectable element. */
.fb-element {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 8px;
    padding: 8px 10px;
    border: 1px solid var(--accent-line);
    border-radius: var(--radius);
    background: var(--accent-soft);
    font-size: 13px;
}

.fb-element__was {
    flex: none;
    color: var(--text-muted);
}

.fb-element__text {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.fb-element__weg {
    flex: none;
    width: 24px;
    height: 24px;
    border: 0;
    border-radius: 9999px;
    background: transparent;
    color: var(--text-muted);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
}

.fb-element__weg:hover {
    background: var(--surface-hover);
}

html.is-elementauswahl,
html.is-elementauswahl * {
    cursor: crosshair !important;
}

.elementauswahl__rahmen {
    position: fixed;
    z-index: 80;
    box-sizing: border-box;
    border: 2px solid var(--signal-red);
    border-radius: 3px;
    background: rgba(193, 57, 27, 0.10);
    pointer-events: none;
}

/* At the bottom, since what it's about usually sits at the top. If it
   covers the marked element, it jumps to the top (`--oben`). */
.elementauswahl__leiste {
    position: fixed;
    bottom: 12px;
    left: 50%;
    z-index: 81;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    max-width: calc(100vw - 24px);
    padding: 8px 10px 8px 14px;
    border-radius: var(--radius);
    background: var(--text);
    color: #fff;
    font-size: 13px;
    box-shadow: 0 6px 18px rgba(17, 28, 39, 0.25);
    transform: translateX(-50%);
    /* Display only: a click passes through it to the page. */
    pointer-events: none;
}

.elementauswahl__hinweis {
    flex: none;
    opacity: 0.7;
}

.elementauswahl__leiste--oben {
    top: 12px;
    bottom: auto;
}

/* The feedback button is pointless during the selection and would sit
   exactly where the bar is. */
html.is-elementauswahl .fabs {
    display: none;
}


.elementauswahl__text {
    flex: 1 1 180px;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The photo count in the task list — icon and number belong together
   and must not wrap. */
.table__photos {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    margin-left: 6px;
    color: var(--text-dim);
    white-space: nowrap;
}

.table__photos svg {
    width: 13px;
    height: 13px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
}

/* --- Rows that should stand out ------------------------------------------------ */

.table tbody tr.is-low td,
.table tbody tr.is-overdue td {
    background: rgba(232, 133, 10, 0.06);
}

/* The one row that decided the deal: the accepted offer in the project
   file. Green instead of orange — this stands out because it's IN
   EFFECT, not because something's stuck (same color as `badge--ang-angenommen`). */
.table tbody tr.is-accepted td {
    background: rgba(47, 143, 87, 0.08);
}

/* The same overdue date, just not in a tinted row: in the post-costing
   it sits as a sub-line in a cell that already carries a statement
   (Feedback #13055). */
.meta--overdue {
    color: var(--signal-orange);
    font-weight: 600;
}

/* --- Training matrix ------------------------------------------------------- */

/* It grows to the right with every training type — on a narrow screen
   it scrolls instead of blowing up the page. */
.tablescroll {
    overflow-x: auto;
}

.table--matrix th,
.table--matrix td {
    vertical-align: top;
    white-space: nowrap;
}

/* --- Small parts ------------------------------------------------------------ */

/* A button that looks like a link — for actions in the middle of text. */
.linkbtn {
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent-dark);
    font: inherit;
    font-size: inherit;
    text-align: left;
    cursor: pointer;
}

.linkbtn:hover {
    text-decoration: underline;
}

/* A row that opens a dialog as a whole (groups under /gruppen) — the
   name inside stays the button for the keyboard. */
.table__row--klick {
    cursor: pointer;
}

.table__row--klick:hover td {
    background: var(--surface-hover);
}

.material + .material {
    margin-top: 16px;
}

.modal__sub {
    margin: 0 0 12px;
    color: var(--text-muted);
    font-size: 13px;
}

/* Only where the sentence announces a consequence you need to know
   before clicking (canceling a paid invoice) — otherwise the color would be decoration. */
.modal__sub--warn {
    color: var(--signal-red);
}

.perms--compact {
    max-height: 220px;
    overflow-y: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0 10px;
}

.perms--compact .perms__row {
    padding: 8px 2px;
}

/* --- A customer's history --------------------------------------------------- */

/* Chronological entries below the input field. Deliberately no speech
   bubbles like in a chat: this is a file, not a conversation. */
.notelist {
    list-style: none;
    margin: 18px 0 0;
    padding: 0;
    border-top: 1px solid var(--border);
}

.notelist li {
    padding: 12px 2px;
    border-bottom: 1px solid var(--border);
}

.notelist__meta {
    margin: 0 0 4px;
    color: var(--text-dim);
    font-size: 12px;
}

/* `white-space: pre-line` preserves the paragraphs someone typed —
   without it a history entry reads as one wall of text. */
.notelist__text {
    margin: 0;
    font-size: 14px;
    line-height: 21px;
    white-space: pre-line;
}

/* Same for the intro and closing note of a locked offer or an issued
   invoice (`_blatttexte.html`). On the PDF a paragraph takes over this
   job — the renderer there doesn't know `pre-line`. */
.blatttext {
    margin: 0;
    white-space: pre-line;
    overflow-wrap: anywhere;
}

/* On narrow viewports the month grid becomes a compact overview: the
   day cells shrink, appointments stay tappable. */
@media (max-width: 860px) {
    .cal__day {
        min-height: 76px;
        padding: 4px;
    }

    .cal__weekday {
        padding: 8px 4px;
    }

    .cal .cal__holiday { /* month sheet only — the week view keeps it */
        display: none;
    }

    .field-row {
        flex-direction: column;
        gap: 0;
    }
}

/* ===========================================================================
   Information — wiki and changelog
   =========================================================================== */

/* Search row: the field takes the space, the button sits beside it. */
.wiki__search {
    display: flex;
    flex: 1;
    align-items: flex-end;
    gap: 12px;
    min-width: 0;
}

/* Page tree. The indentation MUST show the structure — a flat list
   would be exactly what a tree is meant to counter. The line on the
   left makes visible which entries belong together. */
.wikitree {
    margin: 0;
    padding: 0;
    list-style: none;
}

.wikitree .wikitree {
    margin-left: 10px;
    padding-left: 14px;
    border-left: 1px solid var(--border);
}

.wikitree__item {
    padding: 6px 0;
}

.wikitree__link {
    font-weight: 600;
    color: var(--text);
    text-decoration: none;
}

.wikitree__link:hover {
    color: var(--accent-dark);
    text-decoration: underline;
}

.wikitree__meta {
    margin-left: 8px;
    color: var(--text-dim);
    font-size: 12px;
}

/* Quick help on the markup — collapsed, so it's not in the way the
   second time. */
.wikihelp {
    margin-bottom: 16px;
    padding: 8px 12px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-size: 13px;
}

.wikihelp summary {
    cursor: pointer;
    color: var(--text-muted);
}

.wikihelp__list {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 4px 12px;
    margin: 12px 0 4px;
}

.wikihelp__list dt {
    margin: 0;
}

.wikihelp__list dd {
    margin: 0;
    color: var(--text-muted);
}

/* Input field for wiki text: monospace, because the markup works
   line by line and indentation then stays visible. */
.field__input--code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 13px;
    line-height: 1.55;
}

/* The rendered article. Here, exceptionally, element selection applies
   instead of class selection: the HTML is generated by `app/wikitext.py`,
   so it cannot carry classes — and whatever is NOT generated there can't
   show up here either. */
.wiki {
    max-width: 74ch;
    line-height: 1.65;
}

.wiki h2,
.wiki h3,
.wiki h4 {
    margin: 24px 0 8px;
    font-weight: 700;
}

.wiki h2 { font-size: 20px; }
.wiki h3 { font-size: 17px; }
.wiki h4 { font-size: 15px; }

.wiki > :first-child {
    margin-top: 0;
}

.wiki p {
    margin: 0 0 12px;
}

.wiki ul,
.wiki ol {
    margin: 0 0 12px;
    padding-left: 22px;
}

.wiki li {
    margin-bottom: 4px;
}

.wiki blockquote {
    margin: 0 0 12px;
    padding: 10px 14px;
    background: var(--accent-soft);
    border-left: 3px solid var(--accent);
    border-radius: 0 var(--radius) var(--radius) 0;
}

.wiki code {
    padding: 1px 5px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 4px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.9em;
}

.wiki__code {
    margin: 0 0 12px;
    padding: 12px 14px;
    overflow-x: auto;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 13px;
    line-height: 1.55;
}

/* Path from the root to the article. */
.crumbs {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-bottom: 16px;
    color: var(--text-dim);
    font-size: 13px;
}

.crumbs a {
    color: var(--text-muted);
    text-decoration: none;
}

.crumbs a:hover {
    color: var(--accent-dark);
    text-decoration: underline;
}

.crumbs__sep {
    color: var(--border-strong);
}

.crumbs__here {
    color: var(--text);
    font-weight: 600;
}

/* --- Changelog ------------------------------------------------------------- */

.release__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.release__list {
    margin: 12px 0 0;
    padding: 0;
    list-style: none;
}

.release__item {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 8px 0;
    border-top: 1px solid var(--border);
}

/* Fixed column for the badge — so every title starts at the same spot
   and the list reads as a list (modeled on the chip column in
   `AppVersionPage`). */
.release__item .badge {
    flex-shrink: 0;
    width: 124px;
    text-align: center;
}

.release__title {
    min-width: 0;
    word-break: break-word;
}

/* Heading of a department within a release (Feedback #12995). No
   uppercase, no letter-spacing — simply bold like any other heading in
   the app; the blue edge on the left assigns it to the group below
   without opening a second card. */
.release__bereich {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 22px 0 0;
    padding-left: 10px;
    border-left: 3px solid var(--accent);
    font-size: 14px;
    font-weight: 700;
}

/* The first heading sits directly below the release head and doesn't
   need its spacing twice. */
.release__bereich:first-of-type {
    margin-top: 14px;
}

.release__bereich a {
    color: inherit;
    text-decoration: none;
}

.release__bereich a:hover {
    color: var(--accent);
    text-decoration: underline;
}

.release__anzahl {
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 600;
}

/* ===========================================================================
   Miscellaneous — leaderboard, ratings, business card
   =========================================================================== */

/* Your own row stands out from the table — you look for it first
   anyway. */
.table tr.is-me {
    background: var(--accent-soft);
}

/* The first three places get a medal instead of a number. */
.rank {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 9999px;
    color: #3a2c00;
    font-size: 13px;
    font-weight: 700;
}

.rank--1 { background: #f3cf5a; }
.rank--2 { background: #d6dade; }
.rank--3 { background: #ddab74; }

/* --- Ratings --------------------------------------------------------------- */

.cardgrid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 16px;
    margin-bottom: 16px;
}

/* Profile and map of the project file. The map is the narrower half:
   the profile carries ten lines of text, the map only needs enough area
   to show the surroundings. Below the wrap width (`auto-fit`) they stack
   anyway, and then the profile sits on top — the question „who and
   what" comes before „where". */
.cardgrid--akte {
    grid-template-columns: minmax(260px, 3fr) minmax(260px, 2fr);
}

@media (max-width: 900px) {
    .cardgrid--akte {
        grid-template-columns: 1fr;
    }
}

/* The embedded OpenStreetMap map (Feedback #13059). An `iframe`, no
   JavaScript package: this app deliberately has no frontend build, and a
   mapping library would be its first external dependency in the browser.

   The fixed height is deliberate instead of an aspect ratio: an
   `iframe` has no content a height could derive from, and a percentage
   height collapses to 0 in the grid. */
.map {
    margin-top: 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg);
}

.map__frame {
    display: block;
    width: 100%;
    height: 260px;
    border: 0;
}

/* On the map page, the map IS the content, not an addition — there it
   may be taller. */
.map--gross .map__frame {
    height: 420px;
}

/* The caption („Nach Anschrift gefunden") sat on the map's edge. */
.map + .table__sub {
    margin-top: 8px;
}

/* The currently shown site in the list below. Same accent edge as on
   the active navigation entry: it says „you are here" without coloring
   the row (tinted rows mean „overdue" in this app). */
.table tbody tr.is-selected td:first-child {
    box-shadow: inset 3px 0 0 var(--accent);
}

.qrcard {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}

.qrcard__text {
    margin: 0 0 16px;
    color: var(--text-muted);
    font-size: 14px;
}

.qrcard__code {
    margin-bottom: 16px;
}

/* The QR code comes as an SVG without a fixed size (see
   `app/vcard.qr_svg`) — so the CSS sets the size. The white ground covers
   the quiet zone (`QR_RAND`) that is part of the `viewBox`, so the code
   scans on a dark card too. */
.qr {
    display: block;
    width: 180px;
    height: 180px;
    background: var(--qr-grund);
    border-radius: var(--radius);
}

/* --- Business card --------------------------------------------------------- */

.vcard {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 24px;
}

.vcard__body {
    flex: 1;
    min-width: 220px;
}

.vcard__logo {
    height: 26px;
    margin-bottom: 12px;
}

.vcard__name {
    margin: 0;
    font-size: 22px;
    font-weight: 700;
    word-break: break-word;
}

.vcard__role {
    margin: 2px 0 20px;
    color: var(--text-muted);
}

.vcard__rows {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 8px 20px;
    margin: 0;
}

.vcard__rows dt {
    margin: 0;
    color: var(--text-dim);
    font-size: 13px;
}

.vcard__rows dd {
    margin: 0;
    font-weight: 500;
    word-break: break-word;
}

.vcard__qr {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
}

/* On the phone the card stacks; the code stays large enough to scan
   from a neighboring device. */
@media (max-width: 860px) {
    .vcard {
        flex-direction: column;
        align-items: stretch;
    }

    .vcard__qr {
        align-items: flex-start;
    }

    .release__item {
        flex-direction: column;
        gap: 4px;
    }

    .release__item .badge {
        width: auto;
    }
}

/* --- Finance --------------------------------------------------------------- */

/* The vertical rhythm of the content column: 16 px between ANY two blocks.
   It used to exist only between two `.card`s — a card followed by a tile
   grid, a two-column grid, a notice or a heading sat on it with 0 px (17
   places on 10 pages), and nesting cards in a wrapper (`[data-plan]`,
   `[data-sofortsuche-ziel]`) lost it too.

   Only for the content column itself and its class-less wrappers: tiles in
   `.statgrid` or the columns of `.akte` are grid/flex items, a margin there
   would add to their `gap`. Margins collapse here, so a neighbour that
   brings its own (`.pagehead` 24 px, `.notice` 14 px) yields max(), never a
   double gap. `.modal` is fixed and would only shrink by the margin. */
.content > * + *:not(.modal),
.content > :is(div, form):not([class]) > * + *:not(.modal) {
    margin-top: 16px;
}

/* A heading opens a new section: more air above than below (14 px) — also
   inside a card that is split into sections (`.card--abschnitte`). */
.content > * + .section-title,
.content > :is(div, form):not([class]) > * + .section-title,
.card--abschnitte > * + .section-title {
    margin-top: 24px;
}

/* State of an invoice. The draft stays neutral — it's the normal case
   at the start; color only carries what makes a statement. */
.badge--rg-entwurf { background: var(--border); color: var(--text-muted); }
.badge--rg-gestellt { background: var(--accent-soft); color: var(--accent-dark); }
.badge--rg-bezahlt { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--rg-storniert { background: rgba(193, 57, 27, 0.10); color: var(--signal-red-text); }

/* Offers. Same four tones as with the invoice and in the same meaning:
   gray = not sent yet, accent = with the other party, green = done and
   good, red = done and not good. Its own prefix because the statuses
   are named DIFFERENTLY — `angenommen` is not `bezahlt`, and a shared
   class would silently be wrong at the next status. */
.badge--ang-entwurf { background: var(--border); color: var(--text-muted); }
.badge--ang-versendet { background: var(--accent-soft); color: var(--accent-dark); }
.badge--ang-angenommen { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--ang-abgelehnt { background: rgba(193, 57, 27, 0.10); color: var(--signal-red-text); }

/* State of the mailbox (Verwaltung → E-Mail, Feedback #18466). Green =
   works, orange = something is still missing, red = tried and failed.
   Own prefix for the same reason as above. */
.badge--mail-ok { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--mail-offen { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text); }
.badge--mail-fehler { background: rgba(193, 57, 27, 0.10); color: var(--signal-red-text); }

/* State of a RECEIPT (incoming invoice, Feedback #13642). Same rule as
   with the invoice: color only carries what makes a statement — and here
   even the starting state makes one, because „zur Freigabe" means
   „someone has to act". That's why it's the only starting state in the
   app that is colored. */
.badge--bl-erfasst { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text-tief); }
.badge--bl-freigegeben { background: var(--accent-soft); color: var(--accent-dark); }
.badge--bl-bezahlt { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--bl-abgelehnt { background: rgba(193, 57, 27, 0.10); color: var(--signal-red-text); }

/* The early-payment-discount deadline. ⚠️ „Verfallen" (expired)
   deliberately stays GRAY: it's a record, not a call to action — written
   in red it would put an alarm on every older receipt for something
   nobody can change anymore. What's colored is what can still be claimed. */
.badge--sk-offen { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--sk-laeuft_ab { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text-tief); }
.badge--sk-verfallen { background: var(--border); color: var(--text-muted); }
.badge--sk-aus { background: var(--border); color: var(--text-muted); }

/* State of a bank transaction. „Offen" (open) is the normal case right
   after import and stays neutral; the number of open ones is shown as a
   metric above the list anyway. */
.badge--tx-offen { background: var(--border); color: var(--text-muted); }
.badge--tx-zugeordnet { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--tx-ignoriert { background: var(--border); color: var(--text-muted); }

/* ⚠️ A suggestion is NOT an assignment — it therefore looks different
   from „zugeordnet" (assigned; blue instead of green). If both looked
   the same, the list would look as if the app had already booked it; it
   only booked what was unambiguous (`app/abgleich.py`). */
.badge--vorschlag { background: var(--accent-soft); color: var(--accent-dark); }

/* A deduction is the one number in the area you must never overlook —
   it's subtracted from the employee's pay. */
.is-negative {
    color: var(--signal-red);
}

/* Total row below a table (net, VAT, gross). It deliberately sits IN
   the same grid as the line items: a total next to it would have to hit
   the column widths a second time. */
.table tfoot td {
    padding: 8px 16px;
    color: var(--text-muted);
    font-size: 14px;
}

.table tfoot tr:first-child td {
    border-top: 1px solid var(--border);
}

.table tfoot strong {
    color: var(--text);
}

/* Bars of the monthly trend. The width comes ready-made as a percentage
   from the route (`finanzen.py::umsatz`) — a charting package would be
   this app's first frontend build, for twelve numbers. */
.bar {
    height: 8px;
    border-radius: 9999px;
    background: var(--border);
    overflow: hidden;
}

.bar__fill {
    height: 100%;
    border-radius: 9999px;
    background: var(--accent);
}

/* Explanatory sentence below an input field — the open invoice amount,
   the meaning of a pay type. Deliberately not placeholder text: that
   disappears while typing, which is exactly when it's needed. */
.field__hint {
    display: block;
    margin-top: 4px;
    color: var(--text-muted);
    font-size: 12px;
    /* Not the body's 22 px: at 12 px a two-line hint fell apart into two
       loose lines and pushed the next row further down than all others. */
    line-height: 17px;
}

/* ==========================================================================
   „Glas" theme — Feedback #12942
   ==========================================================================
   A SECOND theme, not a replacement: the default above stays unchanged,
   only what changes under `<html data-thema="glass">` is listed here.
   Modeled on Apple's „Liquid Glass" — frosted-glass surfaces behind
   which the background shows through, with a bright light rim at the
   top edge.

   Three decisions carry the whole block:

   1. **The effect hangs on VARIABLES, not on rules.** `--glas` (the
      blur), `--glanz` (the light rim) and `--flaeche` (the colored
      background) are switched off further down at exactly ONE spot —
      the fallback therefore needs no second copy of the rules.
   2. **Glass only on NEUTRAL surfaces.** Everything blue stays opaque:
      white text on a translucent accent loses contrast, and the button
      is the spot where you don't notice it until someone can no longer
      read it.
   3. **Not on lists.** Blur costs compute time per surface;
      `backdrop-filter` on every table row makes scrolling visibly
      sluggish on weak devices. The shell gets glassed — rail, card,
      dialog — not its content.
   ========================================================================== */

html[data-thema="glass"] {
    /* The blur. `saturate` belongs with it: without it, what shows through
       looks washed out instead of filtered. */
    --glas: saturate(180%) blur(20px);
    /* The light rim at the top edge — what makes glass read as glass.
       ALWAYS set as the first entry of a shadow list. */
    --glanz: inset 0 1px 0 rgba(255, 255, 255, 0.75);

    /* The colored background. Frosted glass over light gray looks like
       light gray — it needs something to show through.

       ⚠️ Since Feedback #13114, two of the three gradients follow the
       CHOSEN accent (`--accent-rgb`), no longer fixed to the logo blue.
       Without that, a blue surface would still sit under a berry accent —
       accent red, page blue.

       The orange, by contrast, stays fixed: it comes from the gradient
       in the logo mark and thus belongs to the brand, not to the choice.
       And because „Breuer-Blau" is the shipped default, the block
       unchanged yields the previous gradient — whoever chooses nothing
       sees nothing new. */
    --flaeche:
        radial-gradient(60rem 45rem at 8% -8%, rgba(var(--accent-rgb), 0.26), transparent 62%),
        radial-gradient(45rem 40rem at 98% 4%, rgba(232, 133, 10, 0.16), transparent 60%),
        radial-gradient(55rem 45rem at 62% 108%, rgba(var(--accent-rgb), 0.18), transparent 62%);

    /* Surfaces become translucent, borders become light edges. */
    --surface: rgba(var(--glas-rgb), 0.72);
    --surface-hover: rgba(var(--glas-rgb), 0.92);
    --border: rgba(255, 255, 255, 0.62);
    /* Field borders still need a visible line — a white border on
       whitish glass would be no border at all. */
    --border-strong: rgba(96, 120, 136, 0.32);

    /* The small text now sits on a tinted surface instead of a white
       one. `#6f6f6f` would fall below 4.5:1 there — this value stays
       above it. */
    --text-dim: #545b60;

    /* Wider and softer: glass casts no hard shadow. */
    --shadow: 0 10px 34px rgba(17, 40, 58, 0.12);
    --shadow-card: 0 8px 28px rgba(17, 40, 58, 0.10);
}

/* The background everything sits on. `fixed`, so the sidebar that
   scrolls along filters the same gradient across the whole page —
   without this it loses its coloring while scrolling and looks dead. */
html[data-thema="glass"] body,
html[data-thema="glass"] .login-page {
    background-color: var(--bg);
    background-image: var(--flaeche);
    background-attachment: fixed;
    background-repeat: no-repeat;
}

/* The glassed surfaces — the app's shell, not its content. */
html[data-thema="glass"] .sidebar,
html[data-thema="glass"] .mobilebar,
html[data-thema="glass"] .card,
html[data-thema="glass"] .login-card,
html[data-thema="glass"] .modal__box {
    -webkit-backdrop-filter: var(--glas);
    backdrop-filter: var(--glas);
}

html[data-thema="glass"] .card,
html[data-thema="glass"] .login-card {
    box-shadow: var(--glanz), var(--shadow-card);
}

html[data-thema="glass"] .sidebar {
    /* A bit more transparent than a card: it's the largest surface, and
       the gradient should stay visible through it. */
    background: rgba(var(--glas-rgb), 0.60);
    box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.55);
}

html[data-thema="glass"] .mobilebar {
    background: rgba(var(--glas-rgb), 0.68);
    box-shadow: var(--glanz);
}

/* The dialog sits over a darkened background. If it were as
   translucent as a card, it would filter exactly that darkness and turn
   gray — hence more opaque here. */
html[data-thema="glass"] .modal__box {
    /* The pinned button row doesn't cover here, it VEILS: it carries
       `backdrop-filter: var(--glas)`, so the content visibly blurs away
       beneath it while scrolling instead of disappearing. Opaque, it
       would be a white bar in a translucent dialog. */
    --modal-flaeche: rgba(var(--glas-rgb), 0.72);
    background: rgba(var(--glas-rgb), 0.88);
    box-shadow: var(--glanz), 0 16px 48px rgba(17, 40, 58, 0.28);
}

/* The veil behind the dialog blurs along instead of just darkening —
   that's the effect known from the model. */
html[data-thema="glass"] .modal__backdrop {
    background: rgba(17, 28, 39, 0.30);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
}

/* Table head: the default's light gray would be an opaque bar in the
   middle of a translucent card. */
html[data-thema="glass"] .table thead {
    background: rgba(var(--glas-rgb), 0.45);
}

/* Input fields more opaque than their card — otherwise the field would
   blur with the background and you'd no longer see where you're typing. */
html[data-thema="glass"] .field__input {
    background: rgba(var(--glas-rgb), 0.86);
}

/* Blue stays opaque (point 2 above) — only the light rim is added. */
html[data-thema="glass"] .fab {
    box-shadow: var(--glanz),
                0 2px 6px rgba(17, 28, 39, 0.15);
}

/* --------------------------------------------------------------------------
   Fallback — one spot for three occasions
   --------------------------------------------------------------------------
   Because the effect hangs on `--glas`, `--glanz` and `--flaeche`, it's
   enough to switch off these three and make the surfaces opaque. The
   app stays fully operable, it just looks like the default again.

   `--glanz` is set to an INEFFECTIVE shadow, not to `none`: it sits in
   `box-shadow` lists, and `none` is not a valid entry there — the whole
   declaration would drop out.

   Why each one:
   * `print`  — /rechnungen/<id> is the view printing happens from.
                Frosted glass turns gray on paper, and the gradient
                costs toner for nothing.
   * reduced-transparency — an explicit statement from the operating
                system.
   -------------------------------------------------------------------------- */

@media print, (prefers-reduced-transparency: reduce) {
    html[data-thema="glass"] {
        --glas: none;
        --glanz: 0 0 rgba(0, 0, 0, 0);
        --flaeche: none;
        --surface: #ffffff;
        --surface-hover: #eef1f2;
        --border: #e8ecee;
        --border-strong: #d2d9dc;
        --text-dim: #6f6f6f;
        --shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
        --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.05);
    }

    html[data-thema="glass"] .sidebar,
    html[data-thema="glass"] .mobilebar,
    html[data-thema="glass"] .modal__box,
    html[data-thema="glass"] .field__input {
        background: var(--surface);
    }

    /* Without blur the button row must COVER again — a tinted surface
       would let the scrolled-away text show through. */
    html[data-thema="glass"] .modal__box {
        --modal-flaeche: var(--surface);
    }

    html[data-thema="glass"] .table thead {
        background: var(--bg);
    }

    html[data-thema="glass"] .modal__backdrop {
        background: rgba(17, 28, 39, 0.45);
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }
}

/* Browsers without `backdrop-filter`: the same fallback. Without it
   the surfaces would simply be transparent — text on text. */
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
    html[data-thema="glass"] {
        --glas: none;
        --glanz: 0 0 rgba(0, 0, 0, 0);
        --surface: #ffffff;
        --surface-hover: #eef1f2;
        --border: #e8ecee;
        --border-strong: #d2d9dc;
        --text-dim: #6f6f6f;
    }

    html[data-thema="glass"] .sidebar,
    html[data-thema="glass"] .mobilebar,
    html[data-thema="glass"] .modal__box,
    html[data-thema="glass"] .field__input {
        background: var(--surface);
    }

    /* Without blur the button row must COVER again — a tinted surface
       would let the scrolled-away text show through. */
    html[data-thema="glass"] .modal__box {
        --modal-flaeche: var(--surface);
    }

    html[data-thema="glass"] .table thead {
        background: var(--bg);
    }
}

/* --------------------------------------------------------------------------
   The selection under /einstellungen
   --------------------------------------------------------------------------
   Two tiles with a preview instead of a selection list: „Glas" can be
   described, but not in a way that tells someone in advance what they
   get. The preview shows it. It is deliberately NOT a true picture of
   the app — only surface, rail and card.
   -------------------------------------------------------------------------- */

.themes {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    margin: 0 0 4px;
}

.theme {
    flex: 1 1 200px;
    min-width: 180px;
    /* ⚠️ `flex` instead of `block`, so the preview sits at the TOP. A
       `button` centers its content vertically (UA style), and because
       the two tiles carry descriptions of different length, their
       previews then stood at different heights — side by side in the
       dialog this looks like a misalignment. */
    display: flex;
    flex-direction: column;
    align-items: stretch;
    padding: 12px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    background: var(--surface);
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

.theme:hover {
    border-color: var(--accent);
}

.theme.is-active {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

/* ⚠️ Since Feedback #13114, the preview shows the CHOSEN background and
   the chosen accent (`var(--bg)`, `var(--accent-rgb)`), no longer fixed
   light gray and logo blue. Otherwise the „Glas" tile would claim a
   look the app doesn't actually have — and the three axes are, after
   all, freely combinable. */
.theme__preview {
    display: flex;
    height: 76px;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg);
}

.theme__preview--glass {
    background-color: var(--bg);
    background-image:
        radial-gradient(12rem 9rem at 6% -14%, rgba(var(--accent-rgb), 0.42), transparent 62%),
        radial-gradient(9rem 8rem at 104% 8%, rgba(232, 133, 10, 0.28), transparent 60%);
}

.theme__bar {
    width: 34%;
    background: var(--paper);
    border-right: 1px solid var(--paper-line);
}

.theme__preview--glass .theme__bar {
    background: rgba(var(--glas-rgb), 0.55);
    border-right-color: rgba(255, 255, 255, 0.7);
    -webkit-backdrop-filter: saturate(180%) blur(8px);
    backdrop-filter: saturate(180%) blur(8px);
}

.theme__body {
    flex: 1;
    padding: 10px;
}

.theme__card {
    height: 100%;
    border-radius: 6px;
    background: var(--paper);
    border: 1px solid var(--paper-line);
}

.theme__preview--glass .theme__card {
    background: rgba(var(--glas-rgb), 0.68);
    border-color: rgba(255, 255, 255, 0.75);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8),
                0 4px 14px rgba(17, 40, 58, 0.10);
    -webkit-backdrop-filter: saturate(180%) blur(8px);
    backdrop-filter: saturate(180%) blur(8px);
}

.theme__name {
    margin: 10px 0 0;
    font-weight: 600;
}

.theme__desc {
    margin: 2px 0 0;
    color: var(--text-dim);
    font-size: 12px;
    line-height: 18px;
}

/* --- The „Darstellung" picker (Feedback #13114) ----------------------------
 *
 * The same three groups in two places: in the dialog the button in the
 * user card opens, and in the card under `/einstellungen`. Both embed
 * `_darstellung.html` — a copy would drift apart at the next
 * value.
 *
 * ⚠️ The previews color themselves from THEIR OWN preset, not from the
 * currently active one: the color wheel carries `.swatch--<kennung>`,
 * the tile `.ground--<kennung>` — the same blocks that set
 * `html[data-akzent]` resp. `html[data-grund]`. So a preview cannot
 * possibly show anything other than what it selects.
 * -------------------------------------------------------------------------- */

.waehler__titel {
    margin: 0 0 8px;
    font-size: 13px;
    font-weight: 700;
}

.waehler__gruppe + .waehler__gruppe {
    margin-top: 18px;
}

/* Accent — color wheels. Their color comes from their own preset,
   which sits on the button itself. */
.swatches {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.swatch {
    position: relative;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 0;
    border-radius: 9999px;
    background: var(--accent);
    cursor: pointer;
    box-shadow: inset 0 0 0 1px rgba(17, 28, 39, 0.12);
}

.swatch:hover {
    box-shadow: inset 0 0 0 1px rgba(17, 28, 39, 0.12),
                0 0 0 3px var(--accent-soft);
}

.swatch:focus-visible {
    outline: 2px solid var(--accent-dark);
    outline-offset: 2px;
}

/* The checkmark says „this is the one". A bare ring would be harder to
   find among eight colorful circles, and color alone must not carry this
   information. White on the accent — same pairing as on the buttons. */
.swatch.is-active::after {
    content: '';
    position: absolute;
    left: 13px;
    top: 8px;
    width: 8px;
    height: 15px;
    border: 2px solid #fff;
    border-top: 0;
    border-left: 0;
    transform: rotate(45deg);
}

/* Background — tiles with a preview. The preset sits on the PREVIEW,
   not on the button: otherwise the „Sand" tile would also color its own
   border and label sand-colored and would no longer look like a
   control of the app. */
.grounds {
    display: grid;
    /* 88 px, so the four tiles in the dialog (about 420 px content width)
       fit in ONE row. At 104 px the fourth wrapped and stood alone
       below the other three. */
    grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
    gap: 12px;
}

.ground {
    display: block;
    padding: 8px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.ground:hover {
    border-color: var(--accent);
}

.ground.is-active {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.ground__preview {
    display: block;
    height: 44px;
    padding: 8px;
    border-radius: 6px;
    background: var(--bg);
}

.ground__card {
    display: block;
    height: 100%;
    border-radius: 4px;
    background: var(--paper);
    border: 1px solid var(--paper-line);
}

.ground__name {
    display: block;
    margin-top: 6px;
    font-size: 12px;
    font-weight: 600;
}

/* --- Projects --------------------------------------------------------------
 *
 * State of a project. As with the invoice, only color carries what makes
 * a statement: the inquiry is the normal case at the start and stays
 * neutral, „in Arbeit" is the accent, „abgeschlossen" green, „abgesagt" red.
 */
.badge--pr-anfrage { background: var(--border); color: var(--text-muted); }
.badge--pr-beauftragt { background: rgba(232, 133, 10, 0.12); color: var(--signal-orange-text); }
.badge--pr-in_arbeit { background: var(--accent-soft); color: var(--accent-dark); }
.badge--pr-abgeschlossen { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--pr-abgesagt { background: rgba(193, 57, 27, 0.10); color: var(--signal-red-text); }

/* --- The flow of an order ---------------------------------------------------
 *
 * Appointment, survey, offer, order, execution, invoice, paid — the
 * chain from `app/ablauf.py`. Structured like pvwebapp's vertical
 * stepper (`components/todos/CustomerTodos.js`): STACKED. Seven points
 * side by side don't fit any phone, and progress that scrolls sideways
 * isn't progress.
 *
 * ⚠️ None of this is operable. The points show a state, they don't set
 * one — nothing in this app is checked off (Feedback #13059). The
 * button on the active point leads to the place where the thing happens.
 */
.ablauf {
    margin: 8px 0 0;
    padding: 0;
    list-style: none;
}

.ablauf__punkt {
    display: flex;
    gap: 12px;
    padding-bottom: 14px;
    /* The line to the next point — it sits on the point, not between them,
       so it grows with the line height. It's dropped on the last one
       (`:last-child`), otherwise the chain would end in nothing. */
    position: relative;
}

.ablauf__punkt::before {
    content: "";
    position: absolute;
    left: 11px;
    top: 24px;
    bottom: 0;
    width: 2px;
    background: var(--border);
}

.ablauf__punkt:last-child { padding-bottom: 0; }
.ablauf__punkt:last-child::before { display: none; }

/* The button row below the chain (customer service): without spacing
   „Termin anlegen" stuck to the information of the last point. */
.ablauf + .rowactions {
    margin-top: 20px;
}

/* The marker: filled when done, outlined when open, pale when skipped
   or dropped. */
.ablauf__marke {
    flex: 0 0 auto;
    z-index: 1;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    border: 2px solid var(--border-strong);
    background: var(--surface);
    color: var(--surface);
    font-size: 13px;
    font-weight: 700;
    line-height: 20px;
    text-align: center;
}

.ablauf__punkt--erledigt .ablauf__marke {
    border-color: var(--signal-green);
    background: var(--signal-green);
}

.ablauf__punkt--laeuft .ablauf__marke {
    border-color: var(--accent);
    background: var(--accent-soft);
}

.ablauf__punkt--uebersprungen .ablauf__marke,
.ablauf__punkt--entfaellt .ablauf__marke {
    border-color: var(--border);
    color: var(--text-dim);
}

.ablauf__text { min-width: 0; }

.ablauf__titel {
    margin: 2px 0 0;
    font-size: 14px;
    font-weight: 600;
}

.ablauf__auskunft,
.ablauf__handlung {
    margin: 2px 0 0;
    color: var(--text-muted);
    font-size: 13px;
}

/* The skipped and the dropped point step back: they are information
   about the past, not a call to action. */
.ablauf__punkt--uebersprungen .ablauf__titel,
.ablauf__punkt--entfaellt .ablauf__titel { color: var(--text-dim); }

/* The active point is the only one that wants something. */
.ablauf__punkt.is-aktiv .ablauf__titel { color: var(--text); }
.ablauf__punkt.is-aktiv .ablauf__auskunft { color: var(--text); }

/* The state badges — the same restraint as with the project: color
   only carries what makes a statement. */
.badge--ab-erledigt { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }
.badge--ab-laeuft { background: var(--accent-soft); color: var(--accent-dark); }
.badge--ab-offen { background: var(--border); color: var(--text-muted); }
.badge--ab-uebersprungen { background: var(--border); color: var(--text-dim); }
.badge--ab-entfaellt { background: var(--border); color: var(--text-dim); }

/* Status of a trade (Feedback #15828, `app/models/trade.py`) — same
   colors as the steps: gray waits, accent is running, green is done. Its
   own class per status, so the template and `app.js` build it the same
   way (`badge--gw-<status>`) instead of keeping a mapping twice. */
.badge--gw-offen { background: var(--border); color: var(--text-muted); }
.badge--gw-angefragt,
.badge--gw-beauftragt { background: var(--accent-soft); color: var(--accent-dark); }
.badge--gw-erledigt { background: rgba(47, 143, 87, 0.12); color: var(--signal-green-text); }

/* The milestones above the steps — collapsible like pvwebapp's stepper.
   Collapsing is done with `<details>`: no JavaScript, no CSP exception,
   operable by keyboard. Only the active milestone is expanded on load
   (`_ablauf.html`). */
.meilensteine {
    margin: 8px 0 0;
    padding: 0;
    list-style: none;
}

.meilenstein + .meilenstein { border-top: 1px solid var(--border); }

.meilenstein__kopf {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 10px;
    padding: 10px 0;
    cursor: pointer;
    list-style: none;
}

/* Swap the browser's default arrow for our own — on the right, and
   rotated when open. Safari only knows the pseudo-selector. */
.meilenstein__kopf::-webkit-details-marker { display: none; }

.meilenstein__kopf::after {
    content: "";
    margin-left: auto;
    width: 8px;
    height: 8px;
    border-right: 2px solid var(--text-muted);
    border-bottom: 2px solid var(--text-muted);
    transform: rotate(-45deg);
    transition: transform 0.15s ease;
}

.meilenstein__klappe[open] > .meilenstein__kopf::after { transform: rotate(45deg); }

.meilenstein__kopf .ablauf__marke { color: var(--text-muted); }

.meilenstein--erledigt .meilenstein__kopf .ablauf__marke {
    border-color: var(--signal-green);
    background: var(--signal-green);
    color: var(--surface);
}

.meilenstein--laeuft .meilenstein__kopf .ablauf__marke {
    border-color: var(--accent);
    background: var(--accent-soft);
    color: var(--accent-dark);
}

.meilenstein--uebersprungen .meilenstein__kopf .ablauf__marke,
.meilenstein--entfaellt .meilenstein__kopf .ablauf__marke {
    border-color: var(--border);
    color: var(--text-dim);
}

.meilenstein__titel {
    font-size: 15px;
    font-weight: 600;
    min-width: 0;
}

.meilenstein--uebersprungen .meilenstein__titel,
.meilenstein--entfaellt .meilenstein__titel { color: var(--text-dim); }

.meilenstein__stand {
    color: var(--text-muted);
    font-size: 13px;
}

/* The steps below the head align under the title, not under the marker. */
.meilenstein__klappe > .ablauf {
    margin: 0 0 12px 12px;
}

/* Who is responsible for a step. */
.ablauf__aufgaben {
    margin: 4px 0 0;
    padding: 0;
    list-style: none;
    font-size: 13px;
}

.ablauf__aufgaben li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px 6px;
    min-width: 0;
}

.ablauf__aufgaben li.is-overdue .table__muted { color: var(--signal-red); }

/* --- Planning view (`planung.html`) ----------------------------------------
 *
 * The whole survey on one page. Line items are NOT a table with input
 * fields: five columns of fields don't fit any phone, and input that
 * scrolls sideways isn't input. Every row is a grid that stacks on the
 * phone.
 */
.plangrid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 0 16px;
    margin-top: 12px;
}

.planfotos,
.planzeilen {
    margin: 12px 0 0;
    padding: 0;
    list-style: none;
}

.planfotos__punkt,
.planzeile {
    padding: 12px 0;
    border-top: 1px solid var(--border);
    min-width: 0;
}

.planfotos__kopf {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 8px 12px;
}

.planzeile {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 0.7fr) minmax(0, 0.7fr) minmax(0, 1.6fr) auto;
    align-items: end;
    gap: 0 10px;
}

.planzeile .field { margin-bottom: 0; min-width: 0; }

.planzeile__weg { margin-bottom: 2px; }

@media (max-width: 860px) {
    .planzeile { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px 10px; }
    .planzeile__was,
    .planzeile__notiz { grid-column: 1 / -1; }
    .planzeile__weg { justify-self: start; }
}

/* Construction documentation (`ausfuehrung.html`, Feedback #15908) — one
   row per item, in build order. The state sits in the modifier class
   (`dokupunkt--erledigt` …) and colors the left edge: on the site the
   list is skimmed, not read. */

.dokuliste {
    margin: 12px 0 0;
    padding: 0;
    list-style: none;
}

.dokupunkt {
    padding: 12px 0 12px 12px;
    border-top: 1px solid var(--border);
    border-left: 3px solid transparent;
    min-width: 0;
}

.dokupunkt--erledigt { border-left-color: var(--signal-green); }
.dokupunkt--fehlt_foto { border-left-color: var(--signal-orange); }
.dokupunkt--entfaellt { border-left-color: var(--border); opacity: 0.7; }

.dokupunkt__kopf {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 8px 12px;
}

/* The checkbox is this page's main button — hit it while wearing a glove. */
.dokupunkt__haken {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    cursor: pointer;
    min-width: 0;
}

.dokupunkt__haken input {
    width: 22px;
    height: 22px;
    margin-top: 1px;
    flex: none;
}

.dokupunkt__was { font-weight: 600; }

.dokupunkt--erledigt .dokupunkt__was,
.dokupunkt--entfaellt .dokupunkt__was { font-weight: 500; }

.dokupunkt__bedingung {
    display: inline-block;
    margin-left: 6px;
    color: var(--text-muted);
    font-style: italic;
}

.dokupunkt .field { margin: 8px 0 0; }

/* The button bar sticks to the bottom: with twenty line items,
   „Speichern" shouldn't be three screens away. Left-aligned, because
   every page's feedback button floats bottom right and would otherwise cover it. */
.planleiste {
    position: sticky;
    bottom: 0;
    z-index: 2;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 8px;
    margin-top: 12px;
    padding: 12px 0;
    background: var(--bg);
    border-top: 1px solid var(--border);
}

/* --------------------------------------------------------------------------
   File — the two-column customer page

   Structured after pvwebapp's `Customer.js`: left, narrow, WHO someone
   is and what you do with them; right, wide, WHAT is going on with
   them. There a `window.innerWidth < 1045` listener decides between two
   separately written trees — here a media query over ONE tree does the
   job: two versions of the same content would drift apart at the next
   section, and the narrow one would never get it.
   -------------------------------------------------------------------------- */

.akte {
    display: grid;
    grid-template-columns: minmax(0, 340px) minmax(0, 1fr);
    align-items: start;
    gap: 16px;
}

/* ⚠️ Between 861 px and full width, `.card--flush` clipped AWAY the
   right-hand table columns — its `overflow: hidden` keeps the table away
   from the card corners, and without a scroll area the last column was
   thereby unreachable instead of merely narrow. Below 860 px the same
   rule already fixes this app-wide (see there); in the file it shows up
   earlier because five tables sit in a column narrowed by 340 px.

   That it keeps clipping on the other pages above 860 px is not an
   oversight of this rule but a finding of its own — it deserves a look
   before the app-wide change. */
.akte .card--flush {
    overflow-x: auto;
}

.akte__side,
.akte__main {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
}

/* Same threshold as the model. Below it, the narrow column sits ON TOP:
   whoever pulls up the customer on the phone needs the number first,
   not the order list. */
@media (max-width: 1044px) {
    .akte {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Contact block and archive notice sit IN the customer information, not
   as a separate box above the page: this is read before the call, and
   read here. The edge on the left instead of a surface — a red surface
   in the side column would turn the card into an alarm. */
.akte__warn {
    margin: 14px 0 0;
    padding: 9px 12px;
    border-left: 3px solid var(--signal-red);
    background: rgba(193, 57, 27, 0.06);
    border-radius: 0 var(--radius) var(--radius) 0;
    font-size: 13px;
    line-height: 19px;
    color: var(--signal-red);
}

.akte__warn--mute {
    border-left-color: var(--border-strong);
    background: var(--bg);
    color: var(--text-dim);
}

/* The actions each fill half of the narrow column — two buttons of
   different width side by side read like a ranking that doesn't exist
   between „Kontaktsperre" and „Archivieren". */
.akte__actions {
    display: flex;
    gap: 8px;
}

.akte__actions .btn {
    flex: 1;
    justify-content: center;
}

/* Read-only note: line breaks of the input are preserved. */
.akte__note {
    margin: 0;
    white-space: pre-wrap;
    font-size: 13px;
    line-height: 20px;
}

/* Tighter than the standard data list: at 340 px width a 190-px label
   column would have left hardly any room for the value. */
.datalist--tight {
    grid-template-columns: minmax(0, 108px) minmax(0, 1fr);
    gap: 6px 12px;
}

.datalist--tight dd {
    word-break: break-word;
}

/* Previous/next in the page head. The button without a target STAYS
   instead of disappearing: otherwise the other one would slide under
   the mouse pointer at the moment of clicking. */
.pagenav {
    display: flex;
    gap: 6px;
}

.iconbtn.is-disabled {
    opacity: 0.35;
    cursor: default;
    pointer-events: none;
}


/* --- Customer intake (Feedback #13425) -------------------------------------

   The page for the hand on the phone. It deliberately gets only THREE
   rules: everything else is carried by the existing card and field
   system, and whatever got its own classes here would have to be
   maintained along with every design change. */

/* The cards stack with the same spacing as elsewhere. */
.annahme__treffer,
.annahme__erfassen,
.annahme__suche + .card,
[data-annahme-leer] {
    margin-top: 16px;
}

/* ⚠️ The search field is deliberately larger than a normal
   `.field__input`. It's operated with one hand while the other holds
   the phone — and the target area of a thumb tip is about 44 px. This
   applies at EVERY width: the page is also used at a desk, and a field
   only large on the phone would mean two ways of operating the same thing. */
.annahme__feld {
    min-height: 48px;
    font-size: 17px;
}

/* The result block is a list of buttons, not a table: with three
   people named Meier the address decides, and that fits no column. */
.annahme__treffer .notelist li + li {
    margin-top: 12px;
}

/* --- Units ------------------------------------------------------------------

   The type search in the unit dialog. The catalog has almost five
   hundred entries; a `<select>` list over that would be unusable, so
   you search and pick from a result list. It is a list of BUTTONS and
   not a table — like customer intake and for the same reason: what
   distinguishes one type from the next (series, category, whether docs
   are attached) fits no column. */

.anlage__treffer {
    /* Capped and scrollable: fifty results would otherwise push the
       dialog's button row out of view — the shell does scroll, but then
       you're searching for the save button behind fifty rows. */
    max-height: 240px;
    margin: 0 0 12px;
    padding: 0;
    overflow-y: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    list-style: none;
}

.anlage__treffer li + li {
    border-top: 1px solid var(--border);
}

.anlage__treffer__knopf {
    display: block;
    width: 100%;
    padding: 10px 12px;
    border: none;
    background: none;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.anlage__treffer__knopf:hover,
.anlage__treffer__knopf:focus-visible {
    background: var(--bg);
}

.anlage__treffer__knopf .table__sub {
    /* `display: block` is required, not a style choice: `.table__sub` only
       says „muted", and the line here is a `<span>` inside the button —
       without it the category stuck to the type name („VC 206/5-5Brennwertgeraet"). */
    display: block;
    margin: 2px 0 0;
}

.anlage__gewaehlt {
    margin: 0 0 12px;
    padding: 8px 12px;
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text-dim);
    font-size: 13px;
}

/* The devices under a maintenance contract in the customer file: one
   plaque per category, below it the installed devices as a jump to
   their unit. `overflow-wrap`, because a type like „Vaillant ecoTEC plus
   VC 206/5-5" would otherwise stick out of the cell in a phone's narrow column. */
.wartung__kategorien,
.wartung__geraete {
    margin: 0;
    padding: 0;
    list-style: none;
}

.wartung__kategorie {
    margin-top: 6px;
}

.wartung__geraete {
    margin: 2px 0 0 10px;
    font-size: 13px;
    font-weight: 400;
    line-height: 19px;
    overflow-wrap: anywhere;
}

/* Briefly highlight the unit jumped to — otherwise you land in a list
   of eight devices and have to find the one. `scroll-margin`, so the
   head and column titles of the units section stay above it — at 24 px
   the row stuck to the top edge, and you couldn't see WHERE you had landed. */
[data-anlage-row] {
    scroll-margin-top: 120px;
}

[data-anlage-row]:target {
    background: var(--accent-soft);
}

/* Same for the appointment „Mein Tag" jumps to in the project file
   (Feedback #15789) — an order quickly carries a dozen appointments. */
[data-termin-row] {
    scroll-margin-top: 120px;
}

[data-termin-row]:target {
    background: var(--accent-soft);
}

/* The hint below the empty result list. It stands in place of the
   former „Typ neu anlegen" expander: a type is created in the catalog, not here. */
.anlage__leer {
    margin: -4px 0 12px;
}

/* The „50 von 117 angezeigt" row below the result list (Feedback
   #15963). Close to the list and WITHOUT a divider: it belongs to the
   list and is not a remark of its own — unlike the empty-search hint above it. */
.anlage__mehr {
    margin: -8px 0 12px;
    padding-top: 0;
    border-top: 0;
}

/* --------------------------------------------------------------------------
   Installation planning (`montage.html`, Feedback #15831)
   -------------------------------------------------------------------------- */

/* The matrix employees x half-days. Two cells per day, hence narrow and
   centered — the letter (V/N) is only the label, the statement is the
   color. The card around it scrolls horizontally (`card--flush`). */
.montageplan__wer {
    white-space: nowrap;
    position: sticky;
    left: 0;
    background: var(--surface);
}

.montageplan__tag {
    text-align: center;
    white-space: nowrap;
}

.montageplan__zelle {
    width: 22px;
    padding: 4px 0;
    text-align: center;
    font-size: 11px;
    color: var(--text-muted);
    border-left: 1px solid var(--border);
}

/* Free stays light — the color belongs to what's in the way. */
.montageplan__zelle--frei { background: transparent; }
.montageplan__zelle--termin { background: var(--accent-soft); }
/* A tentative hold is not yet an appointment: same surface, weaker. */
.montageplan__zelle--reserviert { background: rgba(232, 133, 10, 0.16); }
.montageplan__zelle--abwesend { background: rgba(193, 57, 27, 0.14); }
/* Requested leave doesn't block — it's therefore only hinted at. */
.montageplan__zelle--beantragt {
    background: repeating-linear-gradient(
        45deg, transparent, transparent 3px,
        rgba(193, 57, 27, 0.18) 3px, rgba(193, 57, 27, 0.18) 6px);
}

/* The legend above the matrix (Feedback #15844) — collapsed, like the
   wiki help: it's read once, it should never be in the way.

   ⚠️ It sets NO colors of its own. The surfaces carry the cell's
   modifier class in the markup (`montageplan__zelle--*`); only the shape
   is set here. A second color table here would silently go wrong at the
   next change. */
.legende {
    margin: 8px 0 4px;
    font-size: 13px;
}

.legende__kopf {
    cursor: pointer;
    color: var(--text-muted);
    width: fit-content;
}

.legende__liste {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: baseline;
    gap: 6px 12px;
    margin: 10px 0 2px;
}

.legende__liste dt,
.legende__liste dd {
    margin: 0;
}

.legende__liste dd { color: var(--text-muted); }

/* The surface is as large as two matrix cells side by side: the border
   makes even „frei" (transparent) visible as a field. */
.legende__flaeche {
    display: inline-block;
    width: 28px;
    height: 16px;
    border: 1px solid var(--border);
    border-radius: 3px;
    vertical-align: -3px;
}

/* V and N stand on their own — they are the label, not a color. */
.legende__zeichen {
    display: inline-block;
    min-width: 13px;
    text-align: center;
    font-size: 11px;
    color: var(--text-muted);
}

/* A list that's only needed on demand (work order: planned material and
   equipment, operator request from 22.09.2026). Like the legend, a
   `<details>` — no JavaScript, no CSP exception. The head is as tall as
   a button: it's hit with the thumb on the phone. */
.klappe {
    margin: 4px 0 10px;
}

.klappe__kopf {
    cursor: pointer;
    min-height: 36px;
    padding: 8px 0;
    font-weight: 600;
}

.klappe[open] > .klappe__kopf {
    margin-bottom: 4px;
}

/* --- Appearance: light, dark, automatic (Feedback #19232) -----------------
 *
 * Three tiles like the backgrounds, at the top of the picker. The preview
 * shows FIXED tones (light gray/anthracite from `app.css`/`dunkel.css`),
 * not the currently active variables: it explains what „hell" and
 * „dunkel" mean, and must do so even when the respective other one applies.
 * „Automatisch" is split diagonally — like on the iPhone.
 * -------------------------------------------------------------------------- */

.modi {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
    gap: 12px;
}

.modus {
    display: block;
    padding: 8px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.modus:hover {
    border-color: var(--accent);
}

.modus.is-active {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.modus__preview {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 6px;
    height: 44px;
    padding: 0 10px;
    border-radius: 6px;
    border: 1px solid var(--border);
}

.modus__preview--hell {
    --vorschau-zeile: #d2d9dc;
    background: #f5f7f8;
}

.modus__preview--dunkel {
    --vorschau-zeile: #3d464d;
    background: #15191c;
}

.modus__preview--auto {
    --vorschau-zeile: #8a949b;
    background: linear-gradient(135deg, #f5f7f8 50%, #15191c 50%);
}

.modus__zeile {
    display: block;
    height: 6px;
    border-radius: 3px;
    background: var(--vorschau-zeile);
}

.modus__zeile--kurz {
    width: 60%;
}

.modus__name {
    display: block;
    margin-top: 6px;
    font-size: 12px;
    font-weight: 600;
}
