/* Lumeon CRM — the single stylesheet (no build step, no utility framework).
 * CONTRACT: tokens, spacing scale, type roles and the five phase-2 components
 * follow .planning/phases/01-fundament/01-UI-SPEC.md. Hard rules:
 *   - spacing only from the --space-* scale
 *   - four type roles (20/16/14/12) and 2 weights (400/600)
 *   - control size comes from --control-h / --control-text, never a literal.
 *     Under a coarse pointer those resolve to 44px / 16px, which is the
 *     accessible target size and the iOS Safari no-zoom floor. Under a fine
 *     pointer they resolve smaller — a mouse does not need a 44px target.
 *   - no external URL, no embedded webfont, no inline styles. The vhost CSP is
 *     `default-src 'none'` with no `font-src`, so a webfont cannot load at all,
 *     not even self-hosted. Swap --font-sans/--font-mono if that ever changes.
 *
 * 2026-08-27: retuned towards the Fokus-Segmente reference (finer type, tighter
 * spacing, hairline borders, two-layer shadow). See
 * .planning/quick/260827-afe-frontend-feingliedriger-nach-vorbild-05-/
 */

:root {
  /* Color tokens. Names are contractual (test/auth-contract.test.js); the
     values were retuned towards the Fokus-Segmente reference. Two values were
     deliberately NOT taken from it:
       --border-control  the reference's #ccd1da is 1.6:1 on white and fails
                         the 3:1 floor for control boundaries. Fineness here
                         comes from a 1px border, not from a paler one.
       --accent          the reference teal #00c9a7 is 1.8:1 — unusable for
                         text or a filled button. It survives as --brand and
                         as the focus glow, where it is purely decorative. */
  --bg:             #f5f6f8;  /* 60% — app background */
  --surface:        #ffffff;  /* 30% — cards, sidebar, rows, modal */
  --surface-muted:  #eef0f4;  /*       chips, hover rows, table head */
  --border:         #e4e7ec;  /*       decorative separators (hairlines) */
  --border-control: #7d8aa4;  /*       input borders (3.47:1) */
  --text:           #0a0f1e;  /*       body text (18.9:1 on surface) */
  --text-muted:     #5c6a86;  /*       meta, timestamps (5.6:1 on surface) */
  --accent:         #007763;  /* 10% — primary button, focus ring, active nav */
  --accent-soft:    #e6f7f3;  /*       active nav item surface */
  --danger:         #b3261e;  /*       overdue + destructive (6.54:1) */
  --danger-soft:    #fdecec;  /*       overdue section surface */
  --brand:          #00c9a7;  /*       wordmark + focus glow — never text */
  --text-soft:      #2d3650;  /*       labels and secondary prose (10.6:1) */

  /* Focus glow. Decorative only: it sits OUTSIDE the control on top of the
     page background and never carries meaning on its own — the 2px accent
     border does that. */
  --ring: rgba(0, 201, 167, .22);

  /* Font stacks — system only. The vhost CSP is `default-src 'none'` with no
     `font-src`, so a webfont cannot load, not even a self-hosted one. Both
     stacks live here so swapping them later is a one-line change. */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", monospace;

  /* Spacing scale — multiples of 2, tightened for density. --space-md is the
     one that moves the needle: it is the default padding almost everywhere. */
  --space-xs:  4px;
  --space-sm:  8px;
  --space-md:  12px;
  --space-lg:  18px;
  --space-xl:  24px;
  --space-2xl: 36px;
  --space-3xl: 48px;

  /* Control geometry. One place, so density stays coherent — and so the
     coarse-pointer override below only has to touch two numbers. */
  --control-h:    34px;
  --control-text: 14px;

  /* Radius and shadows */
  --radius-sm:   8px;   /* chips (except pill), inputs, buttons */
  --radius-md:   12px;  /* cards, modal */
  --radius-lg:   16px;  /* full-width panes */
  --radius-pill: 999px;
  /* Two layers, after the reference: a hairline that seats the card on the
     page, plus a wide soft drop that gives depth without a grey halo. */
  --shadow-card:  0 1px 2px rgba(10, 15, 30, .05),
                  0 12px 32px -16px rgba(10, 15, 30, .16);
  --shadow-modal: 0 1px 2px rgba(10, 15, 30, .08),
                  0 24px 56px -24px rgba(10, 15, 30, .34);

  /* Weights — exactly two */
  --weight-regular:  400;
  --weight-semibold: 600;
}

/* Touch input gets the full 44px target and 16px fields back. Fineness is a
   mouse-and-keyboard luxury; on a phone it would cost accuracy, and below
   16px iOS zooms the field on focus. */
@media (pointer: coarse) {
  :root {
    --control-h:    44px;
    --control-text: 16px;
  }

  /* Belt and braces. The token above already resolves to 16px here, but a
     text field that renders below 16px makes iOS Safari zoom the page on
     focus and never zoom back out — so the floor is stated outright rather
     than left to depend on one token nobody may touch. */
  .field input,
  .field select,
  .field textarea {
    font-size: 16px;
  }
}

/* ---- base ---------------------------------------------------------------- */

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

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

.sprite {
  display: none;
}

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

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}

/* ---- phase-2 components (01-UI-SPEC lines 249-257) ----------------------- */

/* auth-card: centered, max 400px, 64px viewport margin, 32px padding */
.auth-card {
  max-width: 400px;
  margin: var(--space-3xl) auto;
  background: var(--surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  padding: var(--space-xl);
}

.auth-card h1 {
  /* Page title: 20px / 600 / 1.25 — exactly once per page */
  font-size: 20px;
  font-weight: var(--weight-semibold);
  line-height: 1.2;
  margin: 0 0 var(--space-lg);
}

/* field: visible label above the input */
.field {
  margin-bottom: var(--space-md);
}

.field label {
  display: block;
  font-size: 12.5px;
  font-weight: var(--weight-semibold);
  line-height: 1.4;
  letter-spacing: .005em;
  color: var(--text-soft);
  margin-bottom: var(--space-xs);
}

.field input {
  width: 100%;
  font-family: var(--font-sans);
  font-size: var(--control-text);
  min-height: var(--control-h);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
}

/* Focus on a control: the accent border carries the meaning, the glow only
   makes it easy to spot. Both together are what the reference does; the
   generic :focus-visible outline above still covers links and everything
   that is not a bordered control. */
.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--ring);
}

/* btn-primary: the only button in the auth card, full width */
.btn-primary {
  width: 100%;
  min-height: var(--control-h);
  background: var(--text);
  color: var(--surface);
  border: none;
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  font-weight: var(--weight-semibold);
  cursor: pointer;
}

.btn-primary:disabled {
  opacity: .6;
  cursor: not-allowed;
}

/* banner: full width above the form; error variant carries role="alert" */
.banner {
  width: 100%;
  padding: var(--space-md);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-md);
}

.banner--error {
  background: var(--danger-soft);
  color: var(--danger);
}

.banner--info {
  background: var(--accent-soft);
  color: var(--accent);
}

/* status-text: quiet feedback line under the form (aria-live="polite") */
.status-text {
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--text-muted);
  margin-top: var(--space-md);
}

/* ---- phase-3 components (01-UI-SPEC lines 259-274) ----------------------- */

/* Every state of this phase (modal open, edit mode, active tab, mobile sheet)
 * is a CSS class or an ARIA attribute selector. The vhost CSP is
 * style-src 'self': setAttribute('style', …) and .cssText are blocked at
 * runtime, so a state that has no rule here cannot be expressed at all.
 * Exactly one breakpoint in the whole file: 768px (min) / 767px (max). */

/* app-shell: grid, one column on mobile, 240px sidebar + content from 768px */
.app-shell {
  display: grid;
  grid-template-columns: 1fr;
  min-height: 100vh;
}

/* app-nav: fixed bottom bar below 768px, honouring the iPhone home indicator */
.app-nav {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom);
  z-index: 10;
}

/* app-view: the bottom nav must never cover content (64px bar + safe area) */
.app-view {
  padding: var(--space-md);
  padding-bottom: calc(64px + env(safe-area-inset-bottom));
}

/* nav-item: <a href>, icon + label, 44px hit area — never a div with a handler */
.nav-item {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  flex: 1;
  min-height: var(--control-h);
  padding: var(--space-sm);
  font-size: 13px;
  font-weight: var(--weight-semibold);
  color: var(--text-muted);
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition: background-color 120ms ease-out, color 120ms ease-out;
}

@media (hover: hover) {
  .nav-item:hover {
    background: var(--surface-muted);
    color: var(--text);
  }
}

/* active tab: driven by aria-current alone — one source of truth, not a class */
.nav-item[aria-current="page"] {
  background: var(--text);
  color: var(--surface);
}

/* nav-badge: markup follows in phase 6 (SHELL-04); the rule lives here so the phase-3 component inventory is complete */
.nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  padding: 0 var(--space-xs);
  border-radius: var(--radius-pill);
  background: var(--danger);
  color: var(--surface);
  font-size: 14px;
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
}

/* nav-footer: "Abmelden" inside the sidebar — hidden while the nav is a bar */
.nav-footer {
  display: none;
  margin-top: auto;
  padding: var(--space-md);
}

/* page-header: display title left, exactly one primary button right */
/* Title on the left, its one action on the right. It was a column before,
   which stacked a full-width button under the heading — a lot of furniture for
   a single action. */
.page-header {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-xl);
}

/* Pins the action to the far right whatever sits before it — a title alone on
   the contacts and archive pages, title plus an "Archiviert" chip on a
   customer. Excluding h1 keeps a header that somehow lost its action from
   shoving its own title to the right. */
.page-header > :last-child:not(h1) {
  margin-left: auto;
}

/* page-actions: the wrapper around the three header buttons. It exists so the
   rule above (.page-header > :last-child:not(h1)) pushes the GROUP to the
   right. Without it only the last of the three would move and the other two
   would cling to the title. */
.page-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
}

/* btn-csv: the two CSV buttons. They deliberately do NOT inherit from
   .btn-primary or .btn-secondary. The header rule .page-header .btn-primary
   weighs 0,2,0 while a colour rule .btn-csv--export weighs only 0,1,0 — the
   colour would be there and invisible. That exact failure happened once
   already on 2026-08-27 (the calendar colour grid, noted in
   260827-fuw-SUMMARY.md). Own base, own size rule.

   The position of the three rules below is load-bearing: base FIRST, colours
   AFTER. Same specificity means the later rule wins, and a colour rule placed
   above the base would simply be overwritten.
   test/csv-contract.test.js asserts this order by index, not by reading. */
.btn-csv {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: var(--control-h);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  font-weight: var(--weight-semibold);
  cursor: pointer;
}

/* Export is the green one: --accent is this app's single brand value (primary
   button, focus ring, active nav). A second green for the same job would be a
   second brand value. White on #007763 clears 4.5:1. */
.btn-csv--export {
  background: var(--accent);
  color: var(--surface);
  border-color: var(--accent);
}

/* Import is white with a black frame — the quieter of the two, because it is
   the one that can change data. */
.btn-csv--import {
  background: var(--surface);
  color: var(--text);
  border-color: var(--text);
}

.btn-csv:disabled {
  opacity: .6;
  cursor: not-allowed;
}

/* .btn-primary is full width by birth: it was shaped for the auth card, where
   it is the only thing on screen. In a page header it is a small, quiet
   affordance next to the title. */
.page-header .btn-primary {
  width: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: 28px;
  padding: 0 var(--space-sm);
  font-size: 12.5px;
}

/* Same shrink as .page-header .btn-primary above, same 0,2,0 weight — and
   deliberately SIZE ONLY. A colour here would beat .btn-csv--export/--import
   (0,1,0) and the two buttons would look identical again. */
.page-header .btn-csv {
  min-height: 28px;
  padding: 0 var(--space-sm);
  font-size: 12.5px;
}

/* Touch gets a real target back. This block has to come AFTER the two rules
   above: they weigh the same, so order decides. */
@media (pointer: coarse) {
  .page-header .btn-primary,
  .page-header .btn-csv {
    min-height: var(--control-h);
    padding: 0 var(--space-md);
    font-size: 14px;
  }
}

/* select-bar: the export selection mode. It is the LAST child of the frame,
   after the result container, and is hidden with the `hidden` attribute —
   never with an inline style, which the vhost CSP (style-src 'self') blocks.

   position: sticky, NOT fixed. Sticky stays in the flow, so the bar reserves
   its own space and can never cover the last row of the list — which is the
   row you just ticked before reaching for "Exportieren". A fixed bar is out of
   flow and would need a padding-bottom compensation on the page that has to be
   kept in sync by hand.

   The opaque background is load-bearing, not decoration: rows scroll THROUGH
   this bar, and a translucent one would render the count unreadable. */
.select-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  position: sticky;
  bottom: 0;
  z-index: 1;
  margin-top: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
}

/* The hidden case has to be spelled out: [hidden] { display: none } is a UA
   rule and loses against ANY author rule regardless of specificity, so the
   `display: flex` above would otherwise pin the bar permanently visible — the
   same defect this stylesheet already documents at .search-clear[hidden] and
   .reschedule-panel[hidden]. Found 2026-08-27, after the bar had shipped
   visible outside the selection mode. MUST stay after the rule above. */
.select-bar[hidden] {
  display: none;
}

.select-count {
  font-size: 14px;
  color: var(--text-muted);
  margin-inline-end: auto;
}

/* The "Exportieren" button in the bar keeps its natural width — .btn-primary
   is full width by birth (see above), which would push everything else out. */
.select-bar .btn-primary {
  width: auto;
  padding: 0 var(--space-md);
}

/* select-cell: the checkbox column. 1% makes the table give it exactly the
   width its content needs. Below 768px every cell becomes a block anyway
   (the card list), so the checkbox simply leads the card. */
.select-cell {
  width: 1%;
}

.row-select {
  width: 20px;
  height: 20px;
  accent-color: var(--accent);
  cursor: pointer;
}

.page-header h1 {
  /* Page title: 20px / 600 / 1.25 — exactly once per page */
  font-size: 20px;
  font-weight: var(--weight-semibold);
  line-height: 1.2;
  margin: 0;
}

/* form-actions: stacked on mobile, right aligned from 768px; secondary left */
.form-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin-top: var(--space-lg);
}

/* form-import: Impressum einfuegen (quick 260831-e5l). Sitzt als erstes Kind
   im Formular, direkt unter der Titel-Haarlinie — die echte obere rechte Ecke
   gehoert dem Schliesskreuz (.modal-close sitzt absolut in der Gasse, die
   dialog.modal padding-inline-end freihaelt). Eigene Actions-Regel, weil
   .form-actions column-stacked ist — die Import-Knoepfe stehen nebeneinander. */
.form-import { margin-bottom: var(--space-lg); }
.form-import-head { display: flex; justify-content: flex-end; }
.form-import-panel {
  margin-top: var(--space-md);
  padding: var(--space-md);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.form-import-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}

/* detail-actions: separated action zone at the foot of the detail page (D-45) */
.detail-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  border-top: 1px solid var(--border);
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
}

/* btn-secondary: neutral by contract — "Archivieren" is reversible, not danger */
.btn-secondary {
  min-height: var(--control-h);
  padding: var(--space-sm) var(--space-md);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  font-weight: var(--weight-semibold);
  cursor: pointer;
}

/* btn-danger: only in the destructive confirm dialog (01-UI-SPEC lines 188-194) */
.btn-danger {
  min-height: var(--control-h);
  padding: var(--space-sm) var(--space-md);
  background: var(--danger);
  color: var(--surface);
  border: 0;
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  font-weight: var(--weight-semibold);
  cursor: pointer;
}

.btn-danger:disabled,
.btn-secondary:disabled {
  opacity: .6;
  cursor: not-allowed;
}

/* btn-text: flat text button (accent = text link role, 01-UI-SPEC line 180) */
.btn-text {
  min-height: var(--control-h);
  padding: var(--space-sm);
  background: none;
  border: 0;
  color: var(--accent);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  cursor: pointer;
}

/* customer-table: real table semantics on desktop, <th scope="col"> in markup */
.customer-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

.customer-table th {
  text-align: start;
  font-size: 11.5px;
  font-weight: var(--weight-semibold);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.customer-table thead th {
  background: var(--surface-muted);
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-control);
}

.customer-table td,
.customer-table th {
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--border);
}

@media (hover: hover) {
  .customer-table tbody tr:hover {
    background: var(--surface-muted);
  }
}

.customer-table a {
  color: var(--accent);
  min-height: var(--control-h);
  display: inline-flex;
  align-items: center;
}

/* archived rows: muted text, never a new colour (01-UI-SPEC lines 202-206) */
.customer-table tr.is-archived {
  color: var(--text-muted);
}

/* chip: deliberately colourless — type and status read as words, not colours */
.chip {
  display: inline-flex;
  align-items: center;
  background: var(--surface-muted);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: 12px;
  line-height: 1.4;
  padding: 3px var(--space-sm);
  margin-inline-end: var(--space-xs);
}

/* chip--muted: status "Inaktiv" and archived rows — a withdrawal, not a hue */
.chip--muted {
  color: var(--text-muted);
}

/* field extensions: same 16px/44px contract as the phase-2 input */
.field select,
.field textarea {
  width: 100%;
  font-family: var(--font-sans);
  font-size: var(--control-text);
  min-height: var(--control-h);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
}

.field textarea {
  line-height: 1.5;
  resize: vertical;
}

/* field-checkbox-group: a <fieldset>, so its default chrome is reset */
.field-checkbox-group {
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-md);
}

.field-checkbox-group legend {
  font-size: 14px;
  font-weight: var(--weight-semibold);
  line-height: 1.4;
  padding: 0;
  margin-bottom: var(--space-xs);
}

.field-checkbox-group label {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: var(--control-h);
}

/* field-error: sits under the field, referenced via aria-describedby */
.field-error {
  font-size: 14px;
  line-height: 1.4;
  color: var(--danger);
  margin-top: var(--space-xs);
}

/* field-hint: quiet help text under the field */
.field-hint {
  font-size: 14px;
  line-height: 1.4;
  color: var(--text-muted);
  margin-top: var(--space-xs);
}

.field input[aria-invalid="true"],
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] {
  border-color: var(--danger);
}

/* detail-list: a <dl>, single column on mobile, label column from 768px */
.detail-list {
  columns: 260px auto;
  column-gap: var(--space-2xl);
  margin: 0 0 var(--space-xl);
}

/* dt and dd are flat siblings here, so a grid would tear the pair apart into
   separate columns. Multi-column keeps document order; break-after on the term
   drags its value along instead of stranding it at the top of the next
   column. */
.detail-list dt {
  break-after: avoid;
  font-size: 12.5px;
  font-weight: var(--weight-semibold);
  line-height: 1.4;
  color: var(--text-muted);
}

.detail-list dd {
  break-before: avoid;
  margin: 0 0 var(--space-sm);
}

.detail-list a {
  color: var(--accent);
  min-height: var(--control-h);
  display: inline-flex;
  align-items: center;
}

/* is-editing: the read/edit toggle (D-38) is a class, never an inline style */
.is-editing .detail-list {
  display: none;
}

/* modal: native <dialog> + showModal() — focus trap, Esc and ::backdrop free */
dialog.modal {
  border: 0;
  border-radius: var(--radius-lg);
  /* Extra room on the right so a long title never runs under the close cross,
     which sits in that gutter. */
  padding: var(--space-lg) var(--space-xl);
  padding-inline-end: var(--space-2xl);
  background: var(--surface);
  box-shadow: var(--shadow-modal);
  color: var(--text);
  position: relative;
}

/* modal-close: the cross, in the gutter the padding above reserves. Quiet
   until pointed at — it is an escape hatch, not an action. */
.modal-close {
  position: absolute;
  inset-block-start: var(--space-md);
  inset-inline-end: var(--space-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.modal-close:hover {
  background: var(--surface-muted);
  color: var(--text);
}

@media (pointer: coarse) {
  .modal-close {
    width: var(--control-h);
    height: var(--control-h);
  }
}

dialog.modal[open] {
  animation: modal-in 160ms ease-out;
}

dialog.modal::backdrop,
dialog.nav-menu::backdrop {
  background: rgba(10, 15, 30, .5);
  backdrop-filter: blur(2px);
}

/* iOS Safari background scroll; a class, never body.style.overflow (CSP) */
body.modal-open {
  overflow: hidden;
}

/* confirm-dialog: same shell as the modal, narrower on desktop */
dialog.confirm-dialog {
  /* The close cross is absolutely positioned; without a positioned ancestor it
     would escape the dialog. .modal carries this too. */
  position: relative;
  border: 0;
  border-radius: var(--radius-md);
  padding: var(--space-lg);
  /* After the shorthand, not before — the shorthand would reset it. Reserves
     the gutter the cross sits in. */
  padding-inline-end: var(--space-2xl);
  background: var(--surface);
  box-shadow: var(--shadow-modal);
  color: var(--text);
}

/* modal-title: section role 16px / 600 / 1.3, target of aria-labelledby.
   The hairline turns the dialog into head plus body instead of one flat
   surface — the same device as .note-head and the contacts table. */
.modal-title {
  font-size: 16px;
  font-weight: var(--weight-semibold);
  line-height: 1.25;
  margin: 0 0 var(--space-lg);
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--border);
}

@keyframes modal-in {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* toast-container: bottom centre, aria-live="polite", clear of the bottom nav */
.toast-container {
  position: fixed;
  inset-inline: 0;
  bottom: calc(64px + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  z-index: 20;
  pointer-events: none;
}

/* toast: 5 s, optional "Rückgängig" — inverted surface, no new colour token */
.toast {
  background: var(--text);
  color: var(--surface);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  display: flex;
  align-items: center;
  gap: var(--space-md);
  pointer-events: auto;
  box-shadow: var(--shadow-card);
  animation: modal-in 160ms ease-out;
}

.toast button {
  background: none;
  border: 0;
  color: var(--surface);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  text-decoration: underline;
  min-height: var(--control-h);
  cursor: pointer;
}

/* empty-state: 32px icon, heading, one body sentence, one action */
.empty-state {
  text-align: center;
  padding: var(--space-3xl) var(--space-md);
  color: var(--text);
}

.empty-state h2 {
  font-size: 16px;
  font-weight: var(--weight-semibold);
  line-height: 1.25;
  margin: var(--space-md) 0 var(--space-sm);
}

.empty-state .icon--lg {
  width: 24px;
  height: 24px;
  color: var(--text-muted);
}

/* icon: 20x20 render size (01-UI-SPEC line 67); always aria-hidden in markup */
.icon {
  width: 16px;
  height: 16px;
  flex: none;
}

.icon--lg {
  width: 32px;
  height: 32px;
}

/* ---- phase-4 components (01-UI-SPEC lines 279-284) ----------------------- */

/* search-field: the container around magnifier icon, input and clear button */
.search-field {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  background: var(--surface);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  padding-inline: var(--space-sm);
  margin-top: var(--space-md);
}

/* 16px and 44px are not cosmetic — iOS Safari zooms the page on focus below
   16px, and D-71 autofocuses this field on every entry into the list. */
/* Chrome paints its own clear cross inside type="search". The app already has
   one (.search-clear) that is wired to the search state, carries a German
   aria-label and can be tested — two crosses side by side was just confusing.
   The native one goes, not ours. */
.search-field input::-webkit-search-cancel-button,
.search-field input::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}

.search-field input {
  flex: 1;
  border: 0;
  background: transparent;
  font-size: var(--control-text);
  min-height: var(--control-h);
  color: var(--text);
  font-family: inherit;
}

/* the focus ring moves to the container, because the input has no border of its own */
.search-field input:focus {
  outline: none;
}

.search-field:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

.search-field .icon {
  color: var(--text-muted);
  flex: none;
}

/* search-clear: 44x44 hit area even though the glyph is small (01-UI-SPEC
   accessibility); `hidden` does the showing and hiding, because the CSP
   forbids an inline style (D-70). */
.search-clear {
  border: 0;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  min-width: 44px;
  min-height: var(--control-h);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/* The hidden case has to be spelled out: [hidden] { display: none } is a UA
   rule and loses against ANY author rule regardless of specificity, so the
   display above would otherwise pin the button permanently visible (WR-01). */
.search-clear[hidden] {
  display: none;
}

.search-clear:hover {
  color: var(--text);
}

/* filter-bar: 01-UI-SPEC line 281 — the row scrolls horizontally on mobile
   instead of wrapping, so the chips keep one predictable line */
.filter-bar {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin-top: var(--space-sm);
  padding-bottom: var(--space-xs);
}

/* D-56 — `archived_at` is a different data dimension than `status`, so the
   archive chip sits visually apart instead of being a fourth status */
.filter-bar-separator {
  flex: none;
  width: 1px;
  align-self: stretch;
  background: var(--border);
  margin-inline: var(--space-xs);
}

/* filter-chip: inherits .chip (the JS sets className = 'chip filter-chip');
   this rule only adds what a button needs on top */
.filter-chip {
  border: 1px solid var(--border);
  cursor: pointer;
  font-family: inherit;
  font-size: 14px;
  min-height: 32px;
  white-space: nowrap;
  flex: none;
}

/* the pressed state is driven by aria-pressed alone — one source of truth, the
   screen reader needs the attribute anyway (01-UI-SPEC line 281, T-3-07) */
.filter-chip[aria-pressed="true"] {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}

@media (hover: hover) {
  .filter-chip:hover {
    border-color: var(--border-control);
  }
}

/* list-results: the ONLY subtree that is replaced on every keystroke. The
   frame above it — search field, chips, count — is built once and never
   replaced, otherwise the input is destroyed per letter (Pitfall 4). */
.list-results {
  display: block;
  margin-top: var(--space-md);
}

/* result-count: "12 von 87 Kunden" — label size, muted. Only rendered while a
   query or at least one chip is active (D-69); `hidden` does the hiding. */
.result-count {
  font-size: 14px;
  line-height: 1.4;
  color: var(--text-muted);
  margin-top: var(--space-sm);
}

/* duplicate-callout: the third slot under the company-name field, next to
   .field-hint and .field-error (D-75). Deliberately NEUTRAL: it never blocks
   saving (D-58), so it must not look like an error. role="status", not alert. */
.duplicate-callout {
  font-size: 14px;
  line-height: 1.4;
  color: var(--text);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-sm);
  margin-top: var(--space-xs);
}

.duplicate-callout a {
  color: var(--accent);
  font-weight: var(--weight-semibold);
}

/* Archived matches inside the callout (D-74) reuse the existing .chip--muted —
   do NOT invent a .duplicate-callout--archived variant; the muted chip already
   carries the "a withdrawal, not a hue" role. */

/* ============================================================================
   Phase 5 — Notizen + Folgetermin (NOTE-01 … NOTE-08)
   ========================================================================= */

/* note-section: master data -> note field -> note list (D-79). The field is
   reachable without scrolling, and the note just written appears right below. */
.note-section {
  display: block;
  margin-top: var(--space-lg);
}

/* note-form: permanently open (D-78). Deliberate deviation from the wording of
   NOTE-02, taken by the user after the consequence was named — the page reads
   busier when you only look something up, and speed to the core value wins. */
.note-form {
  display: block;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-md);
}

/* The autogrow is CSS, not JavaScript: field-sizing has been Baseline since
   2026-06-16 (Chrome/Edge 123+, Firefox 152+, Safari + iOS 26.2+) and the
   target browser is Chrome. Zero JS, zero CSP surface, no reflow per keystroke.
   CAVEAT that is easy to miss: under field-sizing the `rows` attribute has NO
   effect (MDN). The "min 5 rows" from 01-UI-SPEC line 293 therefore lives in
   the min-height of 5lh. rows="5" stays in the HTML as the fallback for browsers
   without field-sizing — do not remove it as "ineffective".
   Never set a fixed height here; min/max-height is the documented way. */
.note-form textarea {
  field-sizing: content;
  min-height: 5lh;
  max-height: 60vh;      /* otherwise a long note pushes the list out of view */
  width: 100%;
  box-sizing: border-box;
  font-family: var(--font-sans);
  font-size: var(--control-text);       /* MANDATORY: below 16px iOS Safari zooms in and never back */
  line-height: 1.5;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  padding: var(--space-sm);
  resize: vertical;      /* the user may still drag */
}

/* quickpick-row: three chips plus the free datetime-local field, side by side,
   horizontally scrollable on narrow screens like .filter-bar. */
.quickpick-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}

/* The datetime-local field carries the same 16px contract as every other input
   (01-UI-SPEC line 112). Deliberately NO min attribute in the markup: a time in
   the past is a HINT (.field-hint, line 431), never a blocker. */
.quickpick-row input[type="datetime-local"] {
  font-family: var(--font-sans);
  font-size: var(--control-text);
  min-height: var(--control-h);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-sm);
}

/* prio-row: the marking chips (Termin, Hohe Priorität — 260925-g1a) sit in
   their own line under the quickpicks, inside the same fieldset. Not in
   .quickpick-row: they are not date presets, and reading them as such would
   be the obvious misunderstanding. */
.prio-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}

/* The pressed state carries the danger tokens on purpose: the switch shows
   what it does — this follow-up turns red once it is missed. Must stand AFTER
   .filter-chip[aria-pressed="true"]: same specificity, later rule wins. */
.prio-chip[aria-pressed="true"] {
  background: var(--danger-soft);
  border-color: var(--danger);
  color: var(--danger);
}

/* The pressed Termin chip wears the tokens of the yellow entry row (the
   entry-row--gelb rule further down): it shows what it does — this follow-up
   appears yellow in the week grid (260925-g1a). Same cascade rule as the
   priority chip: must stand AFTER .filter-chip[aria-pressed="true"], same
   specificity, later rule wins. The selector of the yellow row is deliberately
   NOT written out here: test/calendar-view-contract.test.js locates the five
   colour rules by their first occurrence in this file, and a literal in this
   comment would be found before the base rule. */
.termin-chip[aria-pressed="true"] {
  background: #fdf7d9;
  border-color: #7a6100;
  color: #7a6100;
}

/* note-actions: primary left, secondary next to it — one primary action per
   screen (01-UI-SPEC line 401). Also used inside a note-item for "Löschen". */
.note-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

/* Inside the note head the same wrapper is a right-hand anchor, not a footer:
   no top margin, and margin-left pushes it against the right edge. */
.note-head .note-actions {
  margin-top: 0;
  margin-left: auto;
}

/* draft-banner: shown on open when a draft was restored (NOTE-07, line 473).
   Neutral, not an error — nothing went wrong, something was rescued. */
.draft-banner {
  font-size: 14px;
  line-height: 1.4;
  color: var(--text);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-sm);
  margin-bottom: var(--space-sm);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

/* note-results: the ONLY subtree that is ever swapped in the note section. The
   frame above it — draft banner, textarea, quickpicks, save button — is built
   once and never replaced. In phase 4 a rebuild cost a keystroke; here the
   trigger is the minute ticker (D-82), so it would strike mid-sentence, every
   minute, without the user doing anything. */
.note-results {
  display: block;
  margin-top: var(--space-lg);
}

/* note-list: chronological, newest first (NOTE-01). Separation by --border,
   NO zebra striping (01-UI-SPEC line 290). */
.note-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

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

.note-item {
  display: block;
  padding: var(--space-md) 0;
}

/* note-head: one thin line for everything ABOUT the note — stamp, follow-up
   badge, and the delete action pinned to the far right. It wraps rather than
   squeezing the stamp, so a long badge on a narrow screen pushes the button
   onto a second line instead of clipping it. */
.note-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-sm);
  min-height: 22px;
  margin-bottom: var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* note-time: meta size, muted, tabular-nums so stamps line up in a column. */
.note-time {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  margin: 0;
}

/* note-body: the freest user input in the project. pre-wrap keeps the line
   breaks the user typed, 72ch keeps a long note readable (01-UI-SPEC line 119).
   The text arrives as a TEXT NODE, never as markup (T-3-03). */
/* note-delete: btn-text shrunk to the meta line. Quiet until pointed at — a
   delete button sits on every single note and should be reachable, not loud. */
.note-delete {
  min-height: 22px;
  padding: 0 var(--space-xs);
  font-size: 12px;
  font-weight: var(--weight-semibold);
  color: var(--text-muted);
  border-radius: var(--radius-sm);
}

@media (hover: hover) {
  .note-delete:hover:not(:disabled) {
    color: var(--danger);
    background: var(--danger-soft);
  }
}

.note-body {
  font-size: 14px;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;   /* a pasted URL must not widen the page (T-5-35) */
  /* Was 72ch, a pure reading measure. Since the Notion import the note list is
     mostly short conversation entries that get scanned rather than read, and a
     cap that narrow left most of a wide screen empty. Still capped: a line much
     past this loses the reader on the way back to the left edge. */
  max-width: 108ch;
  margin: 0;
  padding-inline: var(--space-sm);
}

/* followup-badge: three states (01-UI-SPEC line 292).
   "Geplant: Sa., 22.08.2026, 09:00" — neutral, the default below.
   "Erledigt" — --text-muted + #i-check.
   "Überfällig" — --danger + #i-alert. */
.followup-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: 14px;
  line-height: 1.4;
  color: var(--text);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px var(--space-sm);
  margin-top: var(--space-xs);
}

.followup-badge--done {
  color: var(--text-muted);
}

/* Overdue is the only red thing in the note list — a statement, not decoration.
   The token pair is the same one the phase-6 overdue section will use. */
.followup-badge--overdue {
  color: var(--danger);
  background: var(--danger-soft);
  border-color: var(--danger);
}

/* note-cap-hint: after 100 rendered notes (NOTE_RENDER_CAP). Notes are loaded
   in full for correctness and rendered capped for performance. The wording is
   its own, because "Suche verfeinern" makes no sense for a note history. */
.note-cap-hint {
  font-size: 14px;
  line-height: 1.4;
  color: var(--text-muted);
  margin-top: var(--space-md);
  text-align: center;
}

/* ============================================================================
   Phase 6 — Kalender + Dashboard (CAL-01 … DASH-04)
   ========================================================================= */

.view-stack {
  display: grid;
  gap: var(--space-lg);
}

.view-header {
  display: grid;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.view-header h1 {
  font-size: 20px;
  font-weight: var(--weight-semibold);
  line-height: 1.2;
  margin: 0;
}

.week-header,
.week-nav {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

.week-header .btn-primary,
.week-header .btn-secondary,
.dashboard-view .btn-secondary,
.entry-modal .btn-primary,
.entry-modal .btn-secondary {
  width: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  text-decoration: none;
}

.week-label {
  margin: 0;
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1.4;
}

.calendar-days,
.calendar-view,
.dashboard-view {
  display: grid;
  gap: var(--space-lg);
}

.day-block,
.dashboard-section {
  display: grid;
  gap: var(--space-sm);
}

.dashboard-section h2 {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg);
  margin: 0;
  font-size: 16px;
  font-weight: var(--weight-semibold);
  line-height: 1.3;
}

/* The day card. .day-block is calendar-only (app/views/kalender.js is the sole
   place that sets it), so styling it directly cannot reach the dashboard.
   overflow:hidden is there for the rounded corners alone — the grid rows below
   grow with their content, so nothing is ever clipped away. */
.day-block {
  gap: 0;
  grid-template-rows: auto 1fr;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

/* The header band — same language as .note-head and the contacts table head:
   a tinted strip that separates what a thing IS from what it CONTAINS.
   formatDayHeader already writes "Montag, 24.08." (and prefixes "Heute — "),
   so the day and its date are in here without any change to the view. */
.day-block h2 {
  margin: 0;
  padding: var(--space-xs) var(--space-md);
  background: var(--surface-muted);
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  font-weight: var(--weight-semibold);
  line-height: 1.45;
  color: var(--text);
}

/* The card carries no padding of its own, so the list brings its own. */
.day-block .entry-list {
  padding: var(--space-sm);
  align-content: start;
}

/* In the week grid a row is one line — time and company, nothing else — so the
   52px floor that suits the dashboard's two- and three-line rows would just
   pad empty space here. */
.day-block .entry-row {
  min-height: 0;
  padding: var(--space-xs) var(--space-sm);
}

/* And it stays one line: a long company name in a narrow column would
   otherwise wrap and undo the very thing this change is for. The row is a
   click target that opens the entry in full, so the ellipsis costs a look,
   not the information. */
.day-block .entry-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* day-empty: the quiet line a day without a follow-up carries. Deliberately
   not an .empty-state — that one is a full section with a heading and a
   button, and seven of them in a week would drown the days that do have
   something. */
.day-empty {
  margin: 0;
  padding: var(--space-xs) var(--space-md);
  font-size: 12.5px;
  color: var(--text-muted);
}

/* Die freie Tagesnotiz: Text ohne Kundenbezug. Bewusst KEINE .entry-row —
   sie hat keine Uhrzeit und ist kein Termin; ein gemeinsamer Look wuerde
   beides genau dort verwechselbar machen, wo der Unterschied zaehlt. Der
   gestrichelte Rand sagt "Merkzettel", nicht "Verabredung". */
.day-note {
  width: 100%;
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr);
  gap: var(--space-sm);
  align-items: center;
  text-align: left;
  padding: var(--space-xs) var(--space-sm);
  border: 1px dashed var(--border-control);
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  cursor: pointer;
}

@media (hover: hover) {
  .day-note:hover {
    border-color: var(--text-muted);
    box-shadow: var(--shadow-card);
  }
}

/* Eine Zeile, wie .day-block .entry-title: in einer schmalen Tagesspalte
   wuerde ein langer Text sonst das ganze Raster auseinanderziehen. Der volle
   Text bleibt im DOM und steht nach dem Klick im Dialog. */
.day-note-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Im Dialog dagegen vollstaendig, mit erhaltenen Zeilenumbruechen. */
.day-note-body {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.entry-list {
  display: grid;
  gap: var(--space-sm);
}

.entry-row {
  width: 100%;
  min-height: 52px;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr) 16px;
  gap: var(--space-sm);
  align-items: center;
  text-align: left;
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--control-text);
}

@media (hover: hover) {
  .entry-row:hover {
    border-color: var(--border-control);
    box-shadow: var(--shadow-card);
  }
}

/* Farbe pro Eintrag. MUSS nach .entry-row stehen: beide sind eine einzelne
   Klasse, also entscheidet allein die Reihenfolge, und die Basisregel setzt
   background und border. Stand dieser Block davor, ueberschrieb .entry-row
   jede Farbe restlos und es war NICHTS zu sehen (genau so passiert, gefixt
   am 2026-08-27) — test/calendar-view-contract.test.js prueft die Reihenfolge
   jetzt und nicht mehr nur die Existenz der Regel.
   Kaskade seit 260831-daj: Farben -> --prio -> --overdue. Alarm heisst
   ueberfaellig UND hohe Prioritaet; nur er traegt noch Rot, und nur er steht
   zuletzt und schlaegt alles. Rot fehlt in der Palette, weil Rot die
   Alarm-Sprache ist.
   Die Randfarben sind dunkel genug fuer Text auf der eigenen Flaeche, die
   Flaechen liegen in der Helligkeit von --danger-soft (#fdecec), damit eine
   gefaerbte Zeile nicht aus dem Raster springt. */
.entry-row--orange { border-left: 3px solid #a85400; background: #fdf0e3; }
.entry-row--gelb   { border-left: 3px solid #7a6100; background: #fdf7d9; }
.entry-row--gruen  { border-left: 3px solid #1c6b3c; background: #e6f4ec; }
.entry-row--blau   { border-left: 3px solid #1a4f9c; background: #e8f0fc; }
.entry-row--lila   { border-left: 3px solid #6b3fa0; background: #f1eafa; }

/* High priority: a red "!" in front of the time, on missed and open
   entries alike. No edge or fill of its own, so a colour label stays visible
   underneath. Stands after the colours and before --overdue (the alarm beats
   everything). */
.entry-row--prio .entry-time::before {
  content: '! ';
  color: var(--danger);
  font-weight: 700;
}

/* THE alarm rule: since 260831-daj this class is set only for overdue entries
   WITH high priority (isAlarm in app/views/kalender.js). The name stays —
   test/calendar-view-contract.test.js and test/domain-sql-parity.test.js pin
   it as the one red rule. */
.entry-row--overdue {
  border-left: 2px solid var(--danger);
  background: var(--danger-soft);
}

.entry-row--overdue .entry-time {
  color: var(--danger);
}

.entry-time {
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
}

.entry-main {
  min-width: 0;
  display: grid;
  gap: var(--space-xs);
}

.entry-title,
.entry-meta,
.entry-overdue {
  overflow-wrap: anywhere;
}

.entry-meta {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.entry-title {
  font-weight: var(--weight-semibold);
}

.entry-meta,
.entry-overdue,
.dashboard-count,
.term-when,
.entry-contact {
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1.4;
}

/* The sentence stays on every overdue row (the user keeps the information),
   but it inherits the muted colour from the group above — the duration still
   stands out through the weight alone. */
.entry-overdue {
  font-weight: var(--weight-semibold);
}

/* Only the alarm case (overdue AND high priority) paints the sentence red. */
.entry-row--overdue .entry-overdue {
  color: var(--danger);
}

.dashboard-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.dashboard-metric {
  min-width: 160px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  padding: var(--space-sm) var(--space-md);
  display: grid;
  gap: var(--space-xs);
  justify-items: end;
}

.dashboard-metric strong {
  font-size: 20px;
  font-weight: var(--weight-semibold);
  line-height: 1.2;
}

.dashboard-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.dashboard-count {
  min-width: 32px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* No danger tokens here since 260831-daj: the heading "Überfällig" and the
   count carry the statement; the surface only groups. A red plane around a
   bucket whose rows are mostly not red anymore would contradict itself. */
.dashboard-section--overdue {
  border-left: 4px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  padding-left: var(--space-md);
  padding-block: var(--space-md);
  padding-right: var(--space-md);
}

dialog.entry-modal {
  width: min(840px, calc(100vw - 32px));
}

dialog.day-note-modal {
  width: min(520px, calc(100vw - 32px));
}

dialog.color-modal {
  width: min(420px, calc(100vw - 32px));
}

dialog.csv-modal {
  width: min(640px, calc(100vw - 32px));
}

.csv-modal-body {
  display: grid;
  gap: var(--space-md);
}

/* The file input keeps the browser's own button — a custom one would be a
   second control for the same job and buys nothing here. The frame matches an
   input field so it reads as part of the form. */
.csv-file {
  width: 100%;
  font-family: var(--font-sans);
  font-size: var(--control-text);
  padding: var(--space-sm);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
}

/* The report can be long. It scrolls INSIDE the dialog, so the confirm button
   below it stays reachable without scrolling the whole sheet. */
.csv-report {
  max-height: 40vh;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-md);
  background: var(--surface-muted);
}

.csv-report-inner {
  display: grid;
  gap: var(--space-md);
}

.csv-report-summary {
  font-weight: var(--weight-semibold);
  margin: 0;
}

.csv-report-section h3 {
  font-size: 13px;
  font-weight: var(--weight-semibold);
  line-height: 1.3;
  margin: 0 0 var(--space-xs);
}

.csv-report-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-muted);
}

.csv-report-more {
  font-style: italic;
}

.day-note-modal-body {
  display: grid;
  gap: var(--space-md);
}

.color-grid {
  display: grid;
  gap: var(--space-sm);
}

/* Ein Knopf je Farbe: links das Farbfeld, rechts der Name. Der Name steht da,
   damit die Wahl nicht allein von der Farbwahrnehmung abhaengt. */
.color-swatch {
  width: 100%;
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  gap: var(--space-sm);
  align-items: center;
  text-align: left;
  min-height: var(--control-h);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  cursor: pointer;
}

/* Das Farbfeld selbst. Fuer den Default-Wert gibt es bewusst KEINE eigene
   Modifier-Regel: er malt nichts, und ein leeres Feld ist genau die richtige
   Vorschau darauf. */
.color-swatch::before {
  content: "";
  width: 24px;
  height: 24px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

@media (hover: hover) {
  .color-swatch:hover {
    border-color: var(--text-muted);
  }
}

/* Die aktuelle Farbe ist markiert, ohne eine eigene Klasse zu erfinden —
   aria-pressed traegt die Information ohnehin fuer Screenreader. */
.color-swatch[aria-pressed="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

.color-swatch--orange::before { background: #fdf0e3; border-color: #a85400; }
.color-swatch--gelb::before   { background: #fdf7d9; border-color: #7a6100; }
.color-swatch--gruen::before  { background: #e6f4ec; border-color: #1c6b3c; }
.color-swatch--blau::before   { background: #e8f0fc; border-color: #1a4f9c; }
.color-swatch--lila::before   { background: #f1eafa; border-color: #6b3fa0; }

.term-modal-body,
.term-summary,
.term-actions-host,
.term-notes,
.term-note-list {
  display: grid;
  gap: var(--space-md);
}

.entry-contact {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

/* Telefon/Telefon 2 and E-Mail/E-Mail 2 side by side once the dialog is wide
   enough; auto-fit instead of a media query because the stylesheet keeps
   exactly one breakpoint (test/shell-contract.test.js). */
.term-contact-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-sm) var(--space-md);
}

.entry-contact span {
  color: var(--text);
  font-weight: var(--weight-semibold);
}

.reschedule-panel {
  display: grid;
  gap: var(--space-sm);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  padding: var(--space-md);
}

.reschedule-panel input[type="datetime-local"] {
  font-family: var(--font-sans);
  font-size: var(--control-text);
  min-height: var(--control-h);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-sm);
}

/* The Termin chip in the reschedule panel (260925-g1a). The panel is a grid,
   and a grid item stretches over the whole column — a chip keeps its own
   width. */
.reschedule-panel .termin-chip {
  justify-self: start;
}

.reschedule-panel[hidden] {
  display: none;
}

/* term-note-field: the mandatory note of the done step in the Termin modal
   (260901-8yu) — textarea plus the shared .quickpick-row with its date field. */
.term-note-field {
  display: grid;
  gap: var(--space-sm);
}

.term-note-field textarea {
  field-sizing: content;
  min-height: 4lh;
  max-height: 40vh;
  width: 100%;
  box-sizing: border-box;
  font-family: var(--font-sans);
  font-size: var(--control-text);   /* below 16px iOS Safari zooms in and never back */
  line-height: 1.5;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  padding: var(--space-sm);
  resize: vertical;
}

.term-note-field input[type="datetime-local"] {
  font-family: var(--font-sans);
  font-size: var(--control-text);   /* stays 16px — only the padding shrinks */
  min-height: var(--control-h);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-xs);
}

/* In the Termin dialog the Termin chip and the priority switch share the
   quickpick row and sit TOGETHER at its right edge, visibly apart from the
   four date presets. They are ONE flex item: the auto margin pushes the pair
   right, and when the row wraps in a narrow dialog the pair wraps as a unit
   instead of being torn apart, one chip left behind on the first line
   (260925-g1a). */
.term-note-field .term-marks {
  display: flex;
  gap: var(--space-sm);
  margin-left: auto;
}

.term-done-offer {
  display: grid;
  gap: var(--space-sm);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  padding: var(--space-md);
}

@media (max-width: 767px) {
  /* customer-card: the SAME table becomes a stacked card list below 768px — one DOM tree, not two (01-UI-SPEC line 231) */
  .customer-table thead {
    display: none;
  }

  .customer-table,
  .customer-table tbody,
  .customer-table tr,
  .customer-table td,
  .customer-table th {
    display: block;
  }

  .customer-table tr {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-card);
    margin-bottom: var(--space-md);
    padding: var(--space-md);
  }

  .customer-table td,
  .customer-table th {
    border-bottom: 0;
    padding: 0 0 var(--space-xs);
  }

  /* full-screen sheet from the bottom, clear of the home indicator */
  dialog.modal,
  dialog.confirm-dialog {
    width: 100%;
    max-width: 100%;
    height: 100%;
    max-height: 100%;
    margin: 0;
    border-radius: 0;
    padding-bottom: calc(var(--space-lg) + env(safe-area-inset-bottom));
  }
}

@media (min-width: 768px) {
  .app-shell {
    grid-template-columns: 208px 1fr;
  }

  .app-nav {
    position: sticky;
    top: 0;
    bottom: auto;
    height: 100vh;
    flex-direction: column;
    border-top: 0;
    border-inline-end: 1px solid var(--border);
    padding-bottom: 0;
  }

  .nav-item {
    justify-content: flex-start;
    gap: var(--space-sm);
    padding: var(--space-xs) var(--space-sm);
    margin: 1px var(--space-sm);
    flex: none;
  }

  .app-view {
    padding: var(--space-xl) var(--space-2xl);
    padding-bottom: var(--space-xl);
  }

  /* The week as a paper planner: Mon/Tue/Wed down the left, Thu/Fri down the
     right, and the weekend sharing the last slot — Saturday and Sunday half a
     weekday tall each, so together they measure exactly one. Six rows make
     that arithmetic honest: a weekday spans two, a weekend day spans one.
     minmax(84px, auto) sets a floor so an empty day still reads as a box, and
     lets a busy one grow instead of hiding entries behind a scrollbar.

     The placement below counts children rather than naming weekdays. That is
     safe here for two reasons that are worth stating: .calendar-days holds
     nothing but day blocks (app/views/kalender.js appends only those), and the
     Monday-to-Sunday order is asserted, not assumed — test/calendar-logic.test.js
     pins the exact key sequence groupEntriesByDay returns. So this rule hangs
     off a checked contract. If that order ever changes, that test goes red
     first. */
  .calendar-days {
    /* Fluid on purpose: the week fills whatever width the screen gives it.
       A cap was tried here and removed — dropping the note preview was about
       slimming what a ROW says, not about shrinking the calendar. */
    grid-template-columns: 1fr 1fr;
    grid-template-rows: repeat(6, minmax(84px, auto));
    gap: var(--space-md);
  }

  .calendar-days > :nth-child(1) { grid-column: 1; grid-row: 1 / span 2; }  /* Montag */
  .calendar-days > :nth-child(2) { grid-column: 1; grid-row: 3 / span 2; }  /* Dienstag */
  .calendar-days > :nth-child(3) { grid-column: 1; grid-row: 5 / span 2; }  /* Mittwoch */
  .calendar-days > :nth-child(4) { grid-column: 2; grid-row: 1 / span 2; }  /* Donnerstag */
  .calendar-days > :nth-child(5) { grid-column: 2; grid-row: 3 / span 2; }  /* Freitag */
  .calendar-days > :nth-child(6) { grid-column: 2; grid-row: 5; }           /* Samstag */
  .calendar-days > :nth-child(7) { grid-column: 2; grid-row: 6; }           /* Sonntag */

  .nav-footer {
    display: block;
  }

  .page-header {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
  }

  .form-actions {
    flex-direction: row;
    justify-content: flex-end;
  }

  .detail-list {
    grid-template-columns: 200px 1fr;
  }

  dialog.modal {
    width: min(760px, calc(100vw - 64px));
    max-width: none;
  }

  dialog.confirm-dialog {
    max-width: 440px;
  }

  .toast-container {
    bottom: var(--space-lg);
  }

  /* Phase 9: the six tiles (five kinds + sum) in one row once there is room.
     Lives in THIS block because the stylesheet has exactly one breakpoint
     (test/shell-contract.test.js) — a second @media would fork it. */
  .ausw-tiles {
    grid-template-columns: repeat(6, 1fr);
  }
}

/* ---- utilities ----------------------------------------------------------- */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- Phase 9 — Auswertung (AUSW-03 … AUSW-05) ---------------------------- */
/* Classes deliberately NOT reinvented here, because the Auswertung should
   look like the calendar by using the calendar's own building blocks:
   .entry-row, .entry-time, .entry-main, .entry-title, .entry-meta,
   .entry-list, .day-block, .empty-state, .chip, .btn-primary, .btn-secondary,
   .week-header, .week-nav, .week-label, .view-stack, .view-header.
   Only what the Auswertung ADDS gets a rule below: the period segment, the
   kind filter, the tiles, the breakdown table and the badge.
   No alarm colour anywhere in this block. Red is the alarm language of this
   app (overdue AND high priority); a completion is never an alarm. */

.auswertung-view {
  display: grid;
  gap: var(--space-lg);
}

/* Tag | Woche | Monat | Jahr. Pressed state via aria-pressed alone — the
   same rule as .nav-item[aria-current] and .color-swatch[aria-pressed]. */
.ausw-modes {
  display: flex;
  gap: var(--space-xs);
  flex-wrap: wrap;
}

.ausw-mode {
  min-height: var(--control-h);
  padding: 0 var(--space-md);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-soft);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  cursor: pointer;
}

@media (hover: hover) {
  .ausw-mode:hover {
    background: var(--surface-muted);
    color: var(--text);
  }
}

.ausw-mode[aria-pressed="true"] {
  background: var(--text);
  color: var(--surface);
  border-color: var(--text);
}

/* Alle | Anrufe | E-Mails | Vor Ort | Nur erledigt. Inherits .chip; pressed
   look mirrors .filter-chip[aria-pressed="true"]. */
.ausw-filter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.ausw-chip {
  min-height: var(--control-h);
  padding: 0 var(--space-md);
  margin-inline-end: 0;
  font-family: var(--font-sans);
  font-size: 14px;
  cursor: pointer;
}

.ausw-chip[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: var(--accent);
}

/* The four numbers plus their sum. Two columns on the phone; the 768px block
   above widens this to five in one row. */
.ausw-summary,
.ausw-tiles {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-sm);
}

.ausw-summary {
  grid-template-columns: 1fr;
}

.ausw-tile {
  display: grid;
  gap: var(--space-xs);
  padding: var(--space-md);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  text-align: left;
}

.ausw-tile-value {
  font-size: 20px;
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.ausw-tile-label {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.4;
}

/* The sum is a derivation, not a fifth kind — it looks different on purpose. */
.ausw-tile--total {
  background: var(--surface-muted);
}

/* A kind tile speaks the colour of its badge, so the number row and the
   badge in the log read as one vocabulary. Same pairs as below. */
.ausw-tile--anruf .ausw-tile-value   { color: #1a4f9c; }
.ausw-tile--email .ausw-tile-value   { color: #6b3fa0; }
.ausw-tile--vor_ort .ausw-tile-value { color: #1c6b3c; }
/* Amber, not red: a move is work, not an alarm (260831-j8c). */
.ausw-tile--verschoben .ausw-tile-value { color: #8a5a12; }
.ausw-tile--erledigt .ausw-tile-value { color: var(--text-soft); }

/* Breakdown per day / per month as a plain table — no chart. Head like the
   contacts table head, cells with a hairline below. */
.ausw-breakdown {
  display: grid;
  gap: var(--space-sm);
}

.ausw-breakdown h2,
.ausw-log h2 {
  margin: 0;
  font-size: 16px;
  font-weight: var(--weight-semibold);
  line-height: 1.3;
}

.ausw-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  font-size: 14px;
}

.ausw-table th,
.ausw-table td {
  padding: var(--space-xs) var(--space-sm);
  border-bottom: 1px solid var(--border);
  text-align: left;
}

.ausw-table thead th {
  background: var(--surface-muted);
  font-size: 12px;
  font-weight: var(--weight-semibold);
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-control);
}

.ausw-table tbody tr:last-child td {
  border-bottom: 0;
}

.ausw-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* The log: day cards from the calendar (.day-block) stacked with the
   calendar's own gap. The day heading inside the card is styled by
   .day-block h2 already. */
.ausw-log {
  margin-top: var(--space-lg);
  display: grid;
  gap: var(--space-lg);
}

/* MUST stand after the .entry-row base rule (line ~1539): both are a single
   class, so order alone decides — the same cascade trap as .entry-row--orange.
   An <a> needs only what a <button> has by default: no underline, inherited
   colour. Everything else IS the calendar row. The badge takes a fourth grid
   column of its own width. */
.ausw-row {
  text-decoration: none;
  color: inherit;
  grid-template-columns: 52px minmax(0, 1fr) auto 16px;
}

/* Kind of a completion as a pill. The three colour pairs are the same as
   .entry-row--blau, --lila and --gruen; "Nur erledigt" is quiet grey. Red is
   missing on purpose — red means alarm in this app (overdue AND high
   priority), and a completion is never an alarm. */
.ausw-badge {
  display: inline-block;
  padding: 2px var(--space-sm);
  border-radius: var(--radius-pill);
  font-size: 11px;
  font-weight: var(--weight-semibold);
  line-height: 1.4;
  white-space: nowrap;
}

.ausw-badge--anruf    { color: #1a4f9c; background: #e8f0fc; }
.ausw-badge--email    { color: #6b3fa0; background: #f1eafa; }
.ausw-badge--vor_ort  { color: #1c6b3c; background: #e6f4ec; }
.ausw-badge--verschoben { color: #8a5a12; background: #fbf0dd; }
.ausw-badge--erledigt { color: var(--text-muted); background: var(--surface-muted); }

/* ---- Erledigt-Art und Wochenkachel (AUSW-02, AUSW-05) -------------------- */
/* The kind chooser in the Termin modal, right above the "Erledigt" button.
   Same skeleton as .ausw-mode; the pressed look is the accent pair of
   .ausw-chip — a choice is an agreement, not a warning, so no alarm colour. */
.done-kind-label {
  margin: 0 0 var(--space-xs);
  font-size: 12px;
  color: var(--text-muted);
}

.done-kind-field {
  margin-bottom: var(--space-md);
}

.done-kind-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.done-kind-option {
  min-height: var(--control-h);
  padding: 0 var(--space-md);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-soft);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  cursor: pointer;
}

.done-kind-option[aria-pressed="true"] {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}

/* The whole group locks while the PATCH is in flight. */
.done-kind-option:disabled {
  opacity: .6;
  cursor: default;
}

/* "Diese Woche" on the dashboard: the .ausw-tiles row inside a link. The
   tiles bring their own look; the link only has to stop being a link. */
.dashboard-week {
  margin-bottom: var(--space-lg);
}

.dashboard-week-link {
  display: block;
  text-decoration: none;
  color: inherit;
}

/* ---------------------------------------------------------------------------
   quick-260901-hzc — Kontakte in Schwarz.
   The list view (and its archive twin) drops the accent for text and frames:
   company links, pressed filter chips, the search-field focus frame, flat text
   buttons and the selection checkboxes read black. Scoped via data-view on
   .app-view (set in shell.js setActive) so Dashboard, Kalender, Kunde and
   Auswertung keep the shared accent rules above untouched. Filled buttons
   (.btn-primary "Neuer Kontakt", .btn-csv--export) are deliberately NOT in
   here — the request was text and borders, not the brand fills.
   Black links and black text buttons need an affordance the colour used to
   carry, hence the underline. Same specificity family (0,2,x) beats every
   base rule (0,1,x) without !important. ------------------------------------ */
.app-view[data-view="kontakte"] .customer-table a,
.app-view[data-view="archiv"] .customer-table a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--border-control);
}

.app-view[data-view="kontakte"] .customer-table a:hover,
.app-view[data-view="archiv"] .customer-table a:hover {
  text-decoration-color: var(--text);
}

.app-view[data-view="kontakte"] .filter-chip[aria-pressed="true"] {
  background: var(--surface);
  border-color: var(--text);
  color: var(--text);
  font-weight: var(--weight-semibold);
}

.app-view[data-view="kontakte"] .search-field:focus-within {
  border-color: var(--text);
  box-shadow: 0 0 0 2px rgba(10, 15, 30, .12);
}

.app-view[data-view="kontakte"] .btn-text,
.app-view[data-view="archiv"] .btn-text {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--border-control);
}

.app-view[data-view="kontakte"] .btn-text:hover,
.app-view[data-view="archiv"] .btn-text:hover {
  text-decoration-color: var(--text);
}

.app-view[data-view="kontakte"] .row-select {
  accent-color: var(--text);
}

/* ---- Phase 10 - Angebote (ANGE-03, ANGE-07) ------------------------------ */
/* Classes deliberately NOT reinvented here, because an Angebot is a form and a
   list like every other screen of this CRM and has to look like one:
   .page-header, .page-actions, .btn-primary, .btn-secondary, .btn-text,
   .chip, .chip--muted, .field, .field-hint, .field-error, .form-actions,
   .customer-table, .result-count, .status-text, .banner, .banner--error,
   .banner--info, .empty-state, .icon, .icon--lg, .detail-actions,
   .visually-hidden.
   Only what the Angebote ADD gets a rule below: the offer list row, the
   Angebotsabschnitt of the customer page, the collapsible blocks of the mask,
   the position cards, the sub-positions and the totals block.
   Two colours DO appear here, and they divide the work between them
   (user request 2026-09-15): green marks the deed - the one button that signs -
   and red marks the state that follows it, locked. Neither value is new to the
   app: #1c6b3c already carries .entry-row--gruen, .color-swatch--gruen,
   .ausw-tile--vor_ort and .ausw-badge--vor_ort, and the red pair is the
   existing --danger/--danger-soft. Red here is not an error message: it says
   "this is closed", the way a sealed envelope is not a complaint. */

/* A colour coat on .btn-secondary, never a button type of its own: .btn-primary
   is width:100% by birth and only shrinks through .page-header .btn-primary, so
   a copy of it would have restated the geometry in a second place. Everything
   here is colour; height, padding, radius and type size stay where they are.
   The hover shade is the same green darkened, not a fifth value in the file. */
.btn-sign {
  background: #1c6b3c;
  border-color: #1c6b3c;
  color: #fff;
}

.btn-sign:hover {
  background: #17572f;
  border-color: #17572f;
}

.btn-sign:disabled {
  opacity: .6;
  cursor: not-allowed;
}

/* Customer page header (user request 2026-09-15): "Angebote" is black with
   white type, "Bearbeiten" is red. Colour coats again, same reasoning as
   .btn-sign — geometry stays with .btn-secondary / .page-header .btn-primary.
   Both come AFTER their base rules on purpose: same 0,1,0 weight, order wins.
   No new values: --text, --danger and --surface are the existing tokens. */
.btn-offers {
  background: var(--text);
  border-color: var(--text);
  color: var(--surface);
}

.btn-edit {
  background: var(--danger);
  color: var(--surface);
}

/* R5, and the FIRST rule of this block: the UA rule [hidden] { display: none }
   loses against ANY author rule that sets display, no matter the specificity.
   This stylesheet has repaired that one class at a time three times already
   (.select-bar, .search-clear, .reschedule-panel). One !important author
   declaration beats every normal author declaration regardless of order, so
   from here on `hidden` simply works — on .page-header .btn-primary, on
   a.btn-secondary, on .offer-toolbar-add and on everything phase 10 adds.
   The three existing per-class counter rules stay where they are; they are
   harmless. No new ones are written. */
[hidden] {
  display: none !important;
}

.offer-view {
  display: grid;
  gap: var(--space-lg);
}

/* Anchors that have to look like buttons: the offer actions navigate
   ("Angebot erstellen", "Stammdaten", "Abbrechen"), so they are <a href> and
   not buttons with a handler. .btn-secondary sets no display, and an inline
   anchor ignores min-height - hence this one rule. */
a.btn-secondary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  text-decoration: none;
  color: var(--text);
}

.offer-meta {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.offer-savestate {
  font-size: 12px;
  color: var(--text-muted);
  margin-inline-end: auto;
}

.offer-num {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.offer-status {
  display: inline-block;
  padding: 2px var(--space-sm);
  border-radius: var(--radius-pill);
  font-size: 11px;
  font-weight: var(--weight-semibold);
  line-height: 1.4;
  white-space: nowrap;
}

.offer-status--entwurf {
  color: var(--text-muted);
  background: var(--surface-muted);
}

/* Signed is LOCKED, and locked is the state the user asked to see at a glance
   (2026-09-15) - in the mask, in the list and in the Angebote section of the
   customer page, all three of which set this one class. The green it used to
   wear moved to .btn-sign, where it marks the deed instead of the state. */
.offer-status--signed {
  color: var(--danger);
  background: var(--danger-soft);
}

.offer-section {
  display: grid;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

.offer-section h2 {
  font-size: 16px;
  font-weight: var(--weight-semibold);
  margin: 0;
}

.offer-rows {
  display: grid;
  gap: 0;
}

.offer-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  gap: var(--space-md);
  align-items: center;
  min-height: var(--control-h);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
}

@media (hover: hover) {
  .offer-row:hover {
    background: var(--surface-muted);
  }
}

.offer-block {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  padding: var(--space-md);
  margin-bottom: var(--space-lg);
}

.offer-block-head {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  min-height: var(--control-h);
  padding: 0;
  background: none;
  border: 0;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 16px;
  font-weight: var(--weight-semibold);
  line-height: 1.3;
  text-align: start;
  cursor: pointer;
}

.offer-block-body {
  display: grid;
  gap: var(--space-md);
  margin-top: var(--space-md);
}

.offer-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

/* display:contents keeps the two buttons and the hint in the toolbar's flex
   flow while still allowing the whole group to disappear on a signed offer -
   the global [hidden] rule at the top of this block does the disappearing. */
.offer-toolbar-add {
  display: contents;
}

.offer-cards {
  display: grid;
  gap: var(--space-md);
}

.offer-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.offer-card-head {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--surface-muted);
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}

.offer-card-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex: 1;
  min-width: 0;
  min-height: var(--control-h);
  padding: 0;
  background: none;
  border: 0;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  font-weight: var(--weight-regular);
  line-height: 1.4;
  text-align: start;
  cursor: pointer;
}

.offer-card-title {
  font-weight: var(--weight-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.offer-card-actions {
  display: flex;
  gap: var(--space-xs);
  margin-inline-start: auto;
}

.offer-card-body {
  display: grid;
  gap: var(--space-md);
  padding: var(--space-md);
}

.offer-grid {
  display: grid;
  gap: var(--space-md);
  grid-template-columns: 1fr;
}

.offer-subs {
  display: grid;
  gap: var(--space-sm);
  padding: var(--space-md);
  background: var(--surface-muted);
  border-radius: var(--radius-sm);
}

.offer-sub {
  align-items: end;
}

/* Same base as .done-kind-option; pressed state via aria-pressed alone - no
   second truth in a class. */
.offer-price-toggle {
  min-width: var(--control-h);
  min-height: var(--control-h);
  padding: 0 var(--space-sm);
  background: var(--surface);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--control-text);
  font-weight: var(--weight-semibold);
  cursor: pointer;
}

.offer-price-toggle[aria-pressed="true"] {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}

.offer-totals {
  display: grid;
  gap: var(--space-sm);
}

.offer-total-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  min-height: var(--control-h);
}

.offer-total-row--grand {
  border-top: 1px solid var(--border-control);
  padding-top: var(--space-sm);
  font-size: 16px;
  font-weight: var(--weight-semibold);
}

/* The two media blocks of phase 10 hang at the END of the file, not inside the
   blocks at app/styles.css:1941 and :1995. A column variant and its base have
   the SAME specificity, so order alone decides - and this stylesheet already
   carries the counter-example: .ausw-tiles stands inside the 768px media block
   AND again as a base rule further down, where the later base rule wins and
   the media rule does nothing. Not repeated here. (The phase-9 case itself is
   deliberately NOT repaired in this phase; it needs its own visual check.)
   The breakpoint is the one single breakpoint the stylesheet has known since
   phase 1: 768px / 767px.
   Pixel literals below are the only ones of this block and the deliberate
   exception from the token rule: the column widths 96/112/144 and
   88/104/120/128 are multiples of 4, 420px caps the totals block, and the 12px
   of the bottom bar is a declared size of the type scale (13px never was). */
@media (min-width: 768px) {
  .offer-block {
    padding: var(--space-lg);
  }

  .offer-grid--2 {
    grid-template-columns: 1fr 1fr;
  }

  .offer-grid--3 {
    grid-template-columns: 1fr 1fr 1fr;
  }

  /* Menge | Einheit | Einzelpreis | Summe */
  .offer-grid--pos {
    grid-template-columns: 96px 112px 1fr 144px;
  }

  /* Bezeichnung | Menge | Einheit | Einzelpreis | Summe | Aktionen */
  .offer-grid--sub {
    grid-template-columns: 1fr 88px 104px 120px 128px auto;
  }

  /* A sub-position without its own price hides TWO .field wrappers. A grid
     item with display:none drops out of auto-placement entirely, so the
     action group would slide into the 120px column and its four 44px buttons
     would overflow. This modifier restates the track list for exactly that
     case; app/views/angebot.js toggles the class together with the two
     hidden attributes (10-14 Task 2 C). */
  .offer-sub--noprice {
    grid-template-columns: 1fr 88px 104px auto;
  }

  .offer-totals {
    max-width: 420px;
    margin-inline-start: auto;
  }

  .customer-table td.offer-num {
    text-align: end;
  }
}

/* ---- phase 11: passkeys (login, shell hint, settings) -------------------- */

/* Tokens only, no new colour. Every class phase 11 sets in the frontend has
   its rule here: the login card (app/views/login.js, plan 11-11), the shell
   notice (app/ui/shell.js, plan 11-13) and the settings page
   (app/views/einstellungen.js, plan 11-05). The whole block, its 767px query
   included, stands BEFORE the phase-10 block below — that one has to stay the
   last 767px block of the file (test/shell-contract.test.js reads it). */

/* auth-passkey: the passkey button above the magic-link form, full width like
   the one button the card had before. */
.auth-passkey {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.auth-passkey .btn-primary {
  width: 100%;
}

/* Once the passkey is offered, the magic-link submit is demoted to
   .btn-secondary (app/views/login.js enablePasskey). That class sets no
   width, so the second way in shrank to its content (152px in the 11-11
   probe) under a full-width passkey button. Same width as the one above. */
.auth-card form .btn-secondary {
  width: 100%;
}

/* auth-divider: "oder per E-Mail-Link" between the two ways in */
.auth-divider {
  margin: var(--space-sm) 0 0;
  text-align: center;
  font-size: 12.5px;
  color: var(--text-muted);
}

/* shell-notice: spans both grid columns of .app-shell (one column on the
   phone, nav + content from 768px) and is therefore the FIRST child of the
   shell, above navigation and content (plan 11-13). */
.shell-notice {
  grid-column: 1 / -1;
  padding: var(--space-md) var(--space-md) 0;
}

/* passkey-hint: the one-time question after a magic-link login — text left,
   its two buttons right, wrapping on a narrow phone. */
.passkey-hint {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-md);
  margin-bottom: 0;
}

.passkey-hint-actions {
  display: flex;
  gap: var(--space-sm);
}

/* settings-view: a reading column, not a full-width table */
.settings-view {
  max-width: 720px;
}

.settings-heading {
  font-size: 16px;
  font-weight: var(--weight-semibold);
  margin: var(--space-lg) 0 var(--space-xs);
}

/* passkey-list: hairline rows — name and meta left, the two text buttons right */
.passkey-list {
  list-style: none;
  margin: var(--space-md) 0 0;
  padding: 0;
  border-top: 1px solid var(--border);
}

.passkey-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--border);
}

.passkey-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.passkey-name {
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

.passkey-meta {
  font-size: 12.5px;
  color: var(--text-muted);
}

.passkey-actions {
  display: flex;
  gap: var(--space-xs);
}

/* The "Einstellungen" link in both footers navigates, so it is an anchor
   with the text-button look — same box as the "Abmelden" button beside it. */
a.btn-text {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

@media (min-width: 768px) {
  .shell-notice {
    padding: var(--space-md) var(--space-2xl) 0;
  }

  /* The sidebar footer stacks its two entries, "Einstellungen" above
     "Abmelden", left-aligned like the nav items. */
  .nav-footer {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-xs);
  }
}

/* Phase-10 layout of the offer rows on the customer page. */
@media (max-width: 767px) {
  .offer-row {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: var(--space-xs);
  }
}

/* ============================================================================
   Phase 12 — Mobile (12-UI-SPEC.md). Section scheme for parallel plans: every
   plan writes ONLY between the start and the end marker of its own section,
   so parallel worktrees merge without conflicts. Owners: 12-A 12-01,
   12-B 12-04, 12-C 12-05, 12-D 12-08, 12-E 12-09, 12-F 12-10, 12-G 12-11,
   12-H 12-06, 12-I 12-01.
   ========================================================================= */
:root {
  --topbar-h: 48px;
}

/* ---- 12-A shell, topbar, menu, safe areas, B-1 ---------------------------- */
/* Below 768px the shell is a BLOCK, not a grid (UI-SPEC § 7). Two reasons,
   both load-bearing: (1) F-1 — as a grid item the content column grew to the
   min-content width of its widest child (1140px on the contact list), so the
   whole page scrolled sideways; a block child never outgrows its container.
   (2) position: sticky only sticks inside its own grid row when the parent is
   a grid — the top bar has to be the child of a block (or flex) container to
   stay at the top while the page scrolls. The phase-3 base rules above (fixed
   bottom bar, 64px view padding, toast clearance) are overridden here, not
   edited; the old bottom-bar .nav-item rules are deleted. */

/* menu-toggle: the 44 x 44 menu button of the top bar (UI-SPEC § 8.1) */
.menu-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--control-h);
  height: var(--control-h);
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  cursor: pointer;
}

.menu-toggle .icon {
  width: 20px;
  height: 20px;
}

/* the overdue badge sits on the button's top-right corner */
.menu-toggle .nav-badge {
  position: absolute;
  inset-block-start: var(--space-xs);
  inset-inline-end: 0;
  min-width: 16px;
  height: 16px;
  padding: 0 var(--space-xs);
  font-size: 12px;
  font-weight: var(--weight-semibold);
}

/* topbar-title: aria-hidden visual repeat of the page's <h1> */
.topbar-title {
  flex: 1;
  min-width: 0;
  font-size: 16px;
  font-weight: var(--weight-semibold);
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* topbar-back: UP-link to the parent section, never history.back() */
.topbar-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-width: 0;
  min-height: var(--control-h);
  padding-inline-end: var(--space-sm);
  font-size: 16px;
  font-weight: var(--weight-semibold);
  line-height: 1.25;
  color: var(--text);
  text-decoration: none;
}

/* dialog.nav-menu: slides in from the left, full height, a 56px strip of the
   dimmed page stays visible and closes it (UI-SPEC § 8.2). The backdrop is
   the shared one of dialog.modal. Laid out only while [open] — without that
   guard a closed dialog would be displayed. */
dialog.nav-menu {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  inset-inline-end: auto;
  margin: 0;
  width: min(320px, calc(100vw - 56px));
  height: 100dvh;
  max-width: none;
  max-height: none;
  border: 0;
  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
  padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom) env(safe-area-inset-left);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-modal);
  overflow-y: auto;
  overscroll-behavior: contain;
}

dialog.nav-menu[open] {
  display: flex;
  flex-direction: column;
  animation: menu-in 200ms ease-out;
}

@keyframes menu-in {
  from { transform: translateX(-100%); }
  to   { transform: translateX(0); }
}

/* The menu has no inner padding for the sheet head's negative margins
   (12-B) to cancel out. */
dialog.nav-menu .modal-head {
  margin: 0;
}

.menu-nav {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: var(--space-xs);
  padding: var(--space-sm);
}

/* Menu rows reuse .nav-item — one truth for aria-current — and override the
   sidebar geometry: left-aligned, full width, control-size text. */
.menu-nav .nav-item {
  display: flex;
  justify-content: flex-start;
  flex: none;
  min-height: var(--control-h);
  padding-inline: var(--space-md);
  gap: var(--space-md);
  font-size: var(--control-text);
  font-weight: var(--weight-regular);
  color: var(--text);
  border-radius: var(--radius-sm);
}

.menu-nav .nav-item[aria-current="page"] {
  background: var(--text);
  color: var(--surface);
  font-weight: var(--weight-semibold);
}

.menu-nav .nav-item .nav-badge {
  margin-inline-start: auto;
}

/* sub-row (Archiv, Stammdaten): its text lines up with the parent's text */
.menu-nav .nav-item--sub {
  padding-inline-start: calc(var(--space-md) * 2 + 16px);
}

.menu-footer {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  margin-top: auto;
  border-top: 1px solid var(--border);
  padding-top: var(--space-sm);
}

/* "Abmelden" is a <button class="nav-item">: reset the button look. font:
   inherit drops the UA button font; the size is then set back to the row
   size so it matches the anchor rows above it. */
.menu-footer button.nav-item {
  background: none;
  border: 0;
  width: 100%;
  font: inherit;
  font-size: var(--control-text);
  text-align: start;
  cursor: pointer;
}

@media (max-width: 767px) {
  /* The ONLY offset for sticky chrome: a focused field or an anchor target
     never ends up under the top bar (UI-SPEC § 10). */
  html {
    scroll-padding-top: calc(var(--topbar-h) + env(safe-area-inset-top) + var(--space-sm));
  }

  .app-shell {
    display: block;
  }

  .app-nav {
    display: none;
  }

  .app-topbar {
    position: sticky;
    top: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    height: calc(var(--topbar-h) + env(safe-area-inset-top));
    padding-top: env(safe-area-inset-top);
    padding-inline: max(var(--space-xs), env(safe-area-inset-left)) max(var(--space-md), env(safe-area-inset-right));
    background: var(--surface);
    border-bottom: 1px solid var(--border);
  }

  .app-view {
    padding: var(--space-md) max(var(--space-md), env(safe-area-inset-right)) calc(var(--space-xl) + env(safe-area-inset-bottom)) max(var(--space-md), env(safe-area-inset-left));
  }

  /* F-1: a single column that can shrink below its content's min width */
  .view-stack,
  .calendar-view,
  .dashboard-view,
  .auswertung-view {
    grid-template-columns: minmax(0, 1fr);
  }

  .toast-container {
    bottom: calc(var(--space-md) + env(safe-area-inset-bottom));
  }

  .toast {
    max-width: calc(100vw - 2 * var(--space-md));
  }
}

@media (min-width: 768px) {
  .app-topbar {
    display: none;
  }

  /* B-1 (UI-SPEC § 8.16): the notice sits above the CONTENT only; the sidebar
     spans both rows and starts at the top edge. Without a notice row 1 is
     empty and 0px tall — the desktop looks exactly as before. */
  .app-shell {
    grid-template-columns: 208px minmax(0, 1fr);
    grid-template-rows: auto 1fr;
  }

  .shell-notice {
    grid-column: 2;
    grid-row: 1;
  }

  .app-nav {
    grid-column: 1;
    grid-row: 1 / span 2;
    padding-left: env(safe-area-inset-left);
  }

  /* iPhone in landscape: clear of the notch; on the Mac this is exactly the
     36px (--space-2xl) of today. */
  .app-view {
    grid-column: 2;
    grid-row: 2;
    padding-right: max(var(--space-2xl), env(safe-area-inset-right));
  }
}
/* ---- end 12-A ---- */

/* ---- 12-B sheets and keyboard --------------------------------------------- */
/* Below 768px every dialog.modal and dialog.confirm-dialog is a sheet from the
   bottom (UI-SPEC § 8.3, D-10) with a sticky head (.modal-head, built by
   app/ui/modal.js) and a sticky foot (.sheet-actions, set by each view on
   exactly one .form-actions). position: sticky only sticks inside a block or
   flex parent — a grid item sticks within its own grid area (Pitfall 1) — so
   the grid bodies of the sheets become flex columns here.
   The three custom properties --vv-height, --vv-top and --keyboard-inset are
   written by app/ui/viewport.js from window.visualViewport, the only way to
   follow the iOS keyboard (D-11); without script the fallbacks in var() apply.
   The class .keyboard-open on <html> comes from the same module
   (classList.toggle('keyboard-open', …)). .sticky-actions is the generic bottom
   bar for page forms (UI-SPEC § 8.4); on desktop it has no effect. */
@media (max-width: 767px) {
  dialog.modal,
  dialog.confirm-dialog {
    position: fixed;
    inset-inline: 0;
    bottom: auto;
    margin: 0;
    top: calc(var(--vv-top, 0px) + env(safe-area-inset-top) + var(--space-sm));
    width: 100%;
    max-width: none;
    height: calc(var(--vv-height, 100dvh) - env(safe-area-inset-top) - var(--space-sm));
    max-height: none;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: 0 max(var(--space-md), env(safe-area-inset-right)) 0 max(var(--space-md), env(safe-area-inset-left));
    overflow-y: auto;
    overscroll-behavior: contain;
    scroll-padding-block: 56px 64px;
  }

  dialog.modal[open],
  dialog.confirm-dialog[open] {
    display: flex;
    flex-direction: column;
    animation: sheet-in 240ms cubic-bezier(.2, .8, .2, 1);
  }

  /* A sheet without a foot (colour picker) keeps clear of the home indicator. */
  :is(dialog.modal, dialog.confirm-dialog):not(:has(.sheet-actions)) {
    padding-bottom: calc(var(--space-md) + env(safe-area-inset-bottom));
  }

  /* R-12: keep every sheet 1px scrollable so overscroll-behavior: contain
     applies on iOS even for short content (no pass-through to the page).
     Absolutely positioned on purpose: an in-flow 1px item is swallowed by the
     growing flex child below (measured: overflow 0). An absolute box whose
     containing block is the (fixed) sheet adds to its scrollable overflow
     without taking any flex space — one pixel taller than the scrollport. */
  :is(dialog.modal, dialog.confirm-dialog)[open]::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 1px;
    height: calc(100% + 1px);
    pointer-events: none;
  }

  /* F-9: short content (day note, CSV) still gets its foot at the bottom edge —
     the child holding the foot grows, the foot's margin-top: auto pushes it
     down. The confirm sheet's foot is a direct child and needs only that. */
  :is(dialog.modal, dialog.confirm-dialog)[open] > :has(> .sheet-actions) {
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
  }

  .term-actions-host,
  .day-note-modal-body,
  .csv-modal-body {
    display: flex;
    flex-direction: column;
  }

  /* The sheet scrolls, not a second box inside it. */
  .csv-report {
    max-height: none;
    overflow: visible;
  }

  /* F-16: title and cross in one flex row — a long title wraps (two lines at
     most) instead of running under the cross. */
  .modal-head {
    position: sticky;
    top: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin: 0 calc(-1 * var(--space-md)) var(--space-md);
    padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-md);
    background: var(--surface);
    border-bottom: 1px solid var(--border);
  }

  .modal-head .modal-title {
    flex: 1;
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .modal-head .modal-close {
    position: static;
    flex: none;
    width: var(--control-h);
    height: var(--control-h);
  }

  /* D-11: Save/Cancel stick to the bottom of the sheet; the sheet itself ends
     at the top of the keyboard (--vv-height), so the foot rides above it.
     Primary on the right via order — visual only, the DOM order stays. */
  .sheet-actions {
    position: sticky;
    bottom: 0;
    z-index: 2;
    margin-top: auto;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin-inline: calc(-1 * var(--space-md));
    padding: var(--space-sm) var(--space-md) calc(var(--space-sm) + env(safe-area-inset-bottom));
    background: var(--surface);
    border-top: 1px solid var(--border);
  }

  .sheet-actions > * {
    flex: 1 1 0;
    min-width: calc(50% - var(--space-xs));
    min-height: var(--control-h);
  }

  .sheet-actions > :is(.btn-primary, .btn-danger) { order: 1; }

  /* UI-SPEC § 8.4: bottom bar of page forms, rides on the keyboard. */
  .sticky-actions {
    position: sticky;
    bottom: var(--keyboard-inset, 0px);
    z-index: 5;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-inline: calc(-1 * var(--space-md));
    padding: var(--space-sm) var(--space-md) calc(var(--space-sm) + env(safe-area-inset-bottom));
    background: var(--surface);
    border-top: 1px solid var(--border);
  }

  /* min-width as in the sheet foot: flex-basis 0 alone left the two buttons
     unequal by their different inline padding (12-13 probe: 172 vs 159 px). */
  .sticky-actions > :is(button, a) {
    flex: 1 1 0;
    min-width: calc(50% - var(--space-xs));
    min-height: var(--control-h);
  }

  .sticky-actions > :is(.btn-primary, .btn-danger) { order: 1; }

  /* The keyboard covers the home indicator: no safe-area padding while open. */
  .keyboard-open .sheet-actions,
  .keyboard-open .sticky-actions {
    padding-bottom: var(--space-sm);
  }

  /* R-14: a toast must not cover a bottom bar for five seconds. */
  body:has(.sticky-actions:not([hidden])) .toast-container {
    bottom: calc(var(--keyboard-inset, 0px) + var(--control-h) + 2 * var(--space-sm) + var(--space-md) + env(safe-area-inset-bottom));
  }

  /* UI-SPEC § 10, page twin of the sheet's scroll-padding-block: a field
     scrolled into view (focus, next-field arrows) ends above the bottom bar
     and the keyboard, not behind them. Measured in the 12-13 probe: without
     it a focused field could stay under the bar of the note or edit form. */
  html:has(.sticky-actions:not([hidden])) {
    scroll-padding-bottom: calc(var(--keyboard-inset, 0px) + var(--control-h) + 2 * var(--space-sm) + env(safe-area-inset-bottom));
  }

  /* Every sheet dims the strip of page above it alike (UI-SPEC § 8.3 sketch);
     the confirm sheet had only the faint UA default. Phone only — the
     desktop confirm dialog keeps its look. */
  dialog.confirm-dialog::backdrop {
    background: rgba(10, 15, 30, .5);
    backdrop-filter: blur(2px);
  }
}

@keyframes sheet-in {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

@media (min-width: 768px) {
  /* Desktop unchanged: title and cross behave as direct children of the dialog. */
  .modal-head { display: contents; }
}
/* ---- end 12-B ---- */

/* ---- 12-C touch targets, 16px floor, tap feedback, fields ----------------- */
/* Global touch and field rules (UI-SPEC § Typography, § 8.13, § 8.15). The
   geometry and the :active feedback live under (pointer: coarse) only, so the
   desktop with a mouse keeps its exact look; the hover rules they replace on
   the phone are wrapped in (hover: hover) where they stand. */

/* iOS enlarges text in landscape on its own otherwise. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* The grey iOS tap flash is replaced by the :active states below. */
a, button, label, summary {
  -webkit-tap-highlight-color: transparent;
}

/* No double-tap zoom on controls; two-finger zoom stays (WCAG 1.4.4). */
a, button, input, select, textarea, label, summary {
  touch-action: manipulation;
}

@media (pointer: coarse) {
  /* D-12 / F-8: every field, inside .field or not — the second and last
     named !important after [hidden]; it must beat any later specificity. */
  input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]),
  select,
  textarea {
    font-size: 16px !important;
  }
  input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]),
  select {
    min-height: var(--control-h);
  }
  /* Falle 7: iOS collapses an empty date field and centres the value. */
  input[type="date"],
  input[type="datetime-local"],
  input[type="time"] {
    min-width: 0;
  }
  .field input[type="date"],
  .field input[type="datetime-local"] {
    width: 100%;
  }
  input::-webkit-date-and-time-value {
    text-align: start;
    min-height: 1.5em;
  }
  /* F-4 … F-7: 44 px touch targets */
  .filter-chip,
  .note-delete,
  .day-block .entry-row,
  .day-note {
    min-height: var(--control-h);
  }
  .entry-contact a {
    display: inline-flex;
    align-items: center;
    min-height: var(--control-h);
  }
  /* The label ("Telefon") stood at the top of the 44 px row while the link
     text sat centred in it (12-13 probe): one text line again. */
  .entry-contact {
    align-items: baseline;
  }
  /* Tap feedback instead of a sticky hover (app/main.js registers the touchstart listener iOS needs). */
  .customer-table tbody tr:active,
  .entry-row:active,
  .day-note:active,
  .offer-row:active,
  .ausw-row:active,
  .menu-nav .nav-item:active {
    background: var(--surface-muted);
  }
  .btn-primary:active,
  .btn-danger:active,
  .btn-sign:active,
  .btn-csv--export:active {
    opacity: .8;
  }
  .btn-secondary:active,
  .chip:active,
  .offer-card-actions .btn-secondary:active {
    background: var(--surface-muted);
  }
}

@media (max-width: 767px) {
  /* UI-SPEC § 8.6: every header action group becomes a full row below the h1
     (Kontakte, Kundenseite, Angebotsmaske). */
  .page-header {
    margin-bottom: var(--space-md);
  }
  .page-header > .page-actions {
    flex-basis: 100%;
    margin-left: 0;
  }
  .page-header > .page-actions > :is(a, button) {
    flex: 1 1 auto;
    min-height: var(--control-h);
  }
}
/* ---- end 12-C ---- */

/* ---- 12-D lists as cards (Kontakte, Archiv) ------------------------------- */
/* Below 768px the SAME table is a card list (UI-SPEC § 8.5, D-07/D-08). The
   card shell itself (surface, border, radius, shadow, padding) comes from the
   phase-3 767px block (.customer-table tr); this section adds the container
   reset, the tappable card, the empty-value rule and the Kontakte/Archiv grid.
   The grid and the hidden cells are scoped to the two routes via data-view:
   the offer list shares .customer-table and gets its own card in 12-G.
   The search row sticks under the top bar and the chip row folds away behind
   "Filter" (UI-SPEC § 8.6, D-09). From 768px nothing here applies except the
   three rules that hide the phone-only controls — the desktop stays a table. */
@media (max-width: 767px) {
  /* The table is only a container on the phone (UI-SPEC § 8.5). */
  .customer-table {
    background: none;
    border: 0;
    box-shadow: none;
    overflow: visible;
  }
  .customer-table tbody tr {
    position: relative;
    margin-bottom: var(--space-sm);
  }
  /* F-2: the row header must not wear the column-header look. */
  .customer-table tbody th {
    text-transform: none;
    letter-spacing: normal;
    font-size: 16px;
    font-weight: var(--weight-semibold);
    color: var(--text);
  }
  /* Whole card tappable: the one link of the row is stretched over the card
     (tr is the containing block). Everything else that is operable on the
     card sits above it — no nested link, no div with a click handler. */
  .customer-table tbody th a::after {
    content: "";
    position: absolute;
    inset: 0;
  }
  .customer-table tbody :is(.card-actions, .select-cell, .restore-cell) {
    position: relative;
    z-index: 1;
  }
  /* The desktop keeps its dash; the card drops the empty line instead. */
  .customer-table [data-empty="true"] {
    display: none;
  }
  :is(.app-view[data-view="kontakte"], .app-view[data-view="archiv"]) .customer-table tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "title actions" "contact actions";
    column-gap: var(--space-sm);
    row-gap: var(--space-xs);
    align-items: center;
  }
  /* Selection mode: an extra leading column only when the cell exists — an
     empty first column would indent every card by the column gap. */
  :is(.app-view[data-view="kontakte"], .app-view[data-view="archiv"]) .customer-table tbody tr:has(> .select-cell) {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "select title actions" "select contact actions";
  }
  .customer-table .cell-company { grid-area: title; padding: 0; }
  /* No contact person: the line is gone, so the title takes both rows and
     sits on the centre line of the action buttons instead of above it. */
  .customer-table tbody tr:has(> .cell-contact[data-empty="true"]) > .cell-company {
    grid-row: 1 / span 2;
  }
  /* Scoped like the black-link rule of the two routes (0,3,1), which would
     otherwise keep its underline on the card title. min-height: 0 drops the
     44px link height of .customer-table a — the whole card is the target, and
     a 44px title box would push the contact line away from the name. */
  :is(.app-view[data-view="kontakte"], .app-view[data-view="archiv"]) .customer-table .cell-company a {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    min-height: 0;
    color: inherit;
    text-decoration: none;
  }
  .customer-table .is-archived .cell-company { color: var(--text-muted); }
  .customer-table .cell-contact {
    grid-area: contact;
    padding: 0;
    font-size: 14px;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* D-08: place, types and status are not on the contact card. */
  :is(.app-view[data-view="kontakte"], .app-view[data-view="archiv"]) .customer-table :is(.cell-city, .cell-types, .cell-status) {
    display: none;
  }
  /* width: auto undoes the desktop 1% column trick, which would resolve
     against the grid track here. */
  .customer-table .select-cell { grid-area: select; padding: 0; width: auto; }
  .customer-table .card-actions,
  .customer-table .restore-cell {
    grid-area: actions;
    display: flex;
    gap: var(--space-sm);
    padding: 0;
  }
  /* Call and mail: 44 x 44 icon links; an empty slot keeps its place so the
     buttons line up across cards (phone left, mail right). */
  .card-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--control-h);
    height: var(--control-h);
    border: 1px solid var(--border-control);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
  }
  .card-action .icon { width: 20px; height: 20px; }
  .card-action--empty { border-color: transparent; background: none; }
  /* 44 x 44 hit area around the 20px selection checkbox (UI-SPEC § 8.15). */
  .row-select-hit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--control-h);
    min-height: var(--control-h);
    cursor: pointer;
  }
  .restore-cell .btn-secondary { min-height: var(--control-h); }
  /* D-09: search always visible and sticky; the chips do not stick along,
     otherwise the sticky area would grow to three lines. */
  .list-toolbar {
    position: sticky;
    top: calc(var(--topbar-h) + env(safe-area-inset-top));
    z-index: 4;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-inline: calc(-1 * var(--space-md));
    padding: var(--space-sm) var(--space-md);
    background: var(--bg);
  }
  /* margin-top: 0 — the field's own top margin would sit inside the row and
     push it below the centre line of the Filter button. */
  .list-toolbar .search-field { flex: 1; min-width: 0; margin-top: 0; }
  .filter-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    flex: none;
    min-height: var(--control-h);
    white-space: nowrap;
  }
  .filter-toggle[aria-expanded="true"] .icon { transform: rotate(180deg); }
  .filter-bar[data-collapsed="true"] { display: none; }
  .filter-bar {
    flex-wrap: wrap;
    overflow-x: visible;
  }
  /* "Archiviert" stays the last chip; the vertical rule would dangle in a
     wrapped row. */
  .filter-bar-separator { display: none; }
}

@media (min-width: 768px) {
  .card-actions,
  .filter-toggle { display: none; }
  /* The search field is a direct flow child again — desktop as before. */
  .list-toolbar { display: contents; }
}
/* ---- end 12-D ---- */

/* ---- 12-E customer page, notes, edit form --------------------------------- */
/* Customer page on the phone (UI-SPEC § 8.7, D-01). app/views/kunde.js marks an
   empty master-data pair with data-empty="true" on dt AND dd; the em dash stays
   in the DOM, so the desktop keeps showing it. The note form's .note-actions and
   the edit form's .form-actions carry .sticky-actions (12-B, D-11): the bar
   sticks above the keyboard, primary on the right via order. */
@media (max-width: 767px) {
  /* F-11: empty master data takes no room on the phone; the note form moves up. */
  .detail-list [data-empty="true"] {
    display: none;
  }
  /* D-14: quickpicks in one row, the native date-time picker on a full row of
     its own below them (the marking chips follow in .prio-row). */
  .note-form .quickpick-row input[type="datetime-local"] {
    flex-basis: 100%;
    width: 100%;
  }
  /* The four quickpicks share the row instead of wrapping "+1 Monat" (measured
     at 390px: 3px short inside the fieldset). The gap alone spaces them. */
  .note-form .quickpick-row > .filter-chip {
    flex: 1 1 auto;
    justify-content: center;
    margin-inline-end: 0;
    padding-inline: var(--space-xs);
  }
  /* The bar spans the form's inner width; the form keeps its own padding. */
  .note-form .sticky-actions {
    margin-top: var(--space-md);
  }
  /* Only as the form's last child: the error banner is appended after the bar,
     and a negative bottom margin would pull it up under the bar. */
  .note-form .sticky-actions:last-child {
    margin-bottom: calc(-1 * var(--space-md));
    border-radius: 0 0 var(--radius-md) var(--radius-md);
  }
  /* .form-actions stacks as a column below 768px; the bar is a row. */
  .note-form .sticky-actions,
  .is-editing .sticky-actions {
    flex-direction: row;
  }
  /* Archivieren · Endgültig löschen: one row, equal width, 44 px. */
  .detail-actions {
    flex-wrap: nowrap;
  }
  .detail-actions > :is(button, a) {
    flex: 1 1 0;
    min-height: var(--control-h);
  }
}
/* ---- end 12-E ---- */

/* ---- 12-F calendar, dashboard --------------------------------------------- */
/* D-06a: the long press has no visible affordance, so touch devices get one
   line that names it. Hidden with a mouse: the right click needs no hint. */
.touch-hint {
  display: none;
}

@media (pointer: coarse) {
  .touch-hint {
    display: block;
    margin: 0;
    font-size: 12px;
    line-height: 1.4;
    color: var(--text-muted);
  }
  /* Falle 8: no magnifier, no text menu while holding a day or an entry. */
  .calendar-days {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }
}

/* Long-press feedback: app/ui/longpress.js sets .is-pressing after 150 ms and
   removes it when the press fires or aborts. Borders and fills only, no new
   colour: the day card darkens its edge, an entry row its own. */
.day-block,
.day-block .entry-list,
.day-block .entry-row {
  transition: border-color 120ms ease-out, background-color 120ms ease-out;
}
.day-block.is-pressing { border-color: var(--border-control); }
.day-block.is-pressing .entry-list { background: var(--surface-muted); }
.day-block .entry-row.is-pressing { border-color: var(--text-muted); }

@media (max-width: 767px) {
  /* F-10: one row [‹] [Heute] [›]; the side labels stay the accessible name.
     The Auswertung's period navigation carries the same class and gets the
     same row. */
  .week-nav {
    display: grid;
    grid-template-columns: var(--control-h) minmax(0, 1fr) var(--control-h);
    gap: var(--space-sm);
  }
  .week-nav > :is(a, button) {
    min-height: var(--control-h);
    justify-content: center;
  }
  .week-nav > :is(:first-child, :last-child) {
    padding: 0;
  }
  .week-nav > :is(:first-child, :last-child) > span {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  /* R-1: a sticky heading only sticks inside a block/flex parent. */
  .dashboard-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
  }
  .dashboard-section h2 {
    top: calc(var(--topbar-h) + env(safe-area-inset-top));
  }
  /* Measured in the render probe: the h2 sits inside .dashboard-section-header,
     a flex row exactly as tall as the heading, so the h2 alone has no room to
     stick. The header row (heading + count) sticks instead, inside the flex
     section above, on the surface of its own section. */
  .dashboard-section-header {
    position: sticky;
    top: calc(var(--topbar-h) + env(safe-area-inset-top));
    z-index: 1;
    background: var(--bg);
  }
  .dashboard-section--overdue .dashboard-section-header {
    background: var(--surface-muted);
  }
}
/* ---- end 12-F ---- */

/* ---- 12-G offers: list, mask, master data --------------------------------- */
/* Offers on the phone (UI-SPEC § 8.5 "Angebotskarte", § 8.11, D-02/D-04).
   List: the same <table class="customer-table"> becomes one card per offer —
   customer and status pill on top, number, date and net total below. The card
   base (surface, frame, stretched number link) comes from the generic card
   rules for every .customer-table (12-D); only the grid of the offer card
   lives here, scoped to the list route so Kontakte and Archiv keep theirs.
   app/views/angebote.js names the cells (.cell-number … .cell-status). */
@media (max-width: 767px) {
  /* The list header keeps its single action on the title row (UI-SPEC § 8.11). */
  .app-view[data-view="angebote"] .page-header > .page-actions {
    flex-basis: auto;
    margin-left: auto;
  }
  .app-view[data-view="angebote"] .customer-table tbody tr {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "customer customer status" "number date total";
    column-gap: var(--space-sm);
    row-gap: var(--space-xs);
    align-items: baseline;
  }
  .app-view[data-view="angebote"] .customer-table .cell-customer {
    grid-area: customer;
    padding: 0;
    font-size: 16px;
    font-weight: var(--weight-semibold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .app-view[data-view="angebote"] .customer-table tbody th.cell-number {
    grid-area: number;
    padding: 0;
    font-size: 14px;
    font-weight: var(--weight-regular);
    font-variant-numeric: tabular-nums;
    text-transform: none;
    letter-spacing: normal;
  }
  .app-view[data-view="angebote"] .customer-table .cell-date {
    grid-area: date;
    padding: 0;
    font-size: 14px;
    color: var(--text-muted);
  }
  .app-view[data-view="angebote"] .customer-table .cell-total {
    grid-area: total;
    justify-self: end;
    padding: 0;
    font-size: 14px;
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
  }
  .app-view[data-view="angebote"] .customer-table .cell-status {
    grid-area: status;
    justify-self: end;
    padding: 0;
  }
}

/* Mask and master data (UI-SPEC § 8.4, § 8.11): on the phone "Speichern"
   moves from the page header into .offer-savebar, the last child of
   .offer-view, which rides above the keyboard through .sticky-actions (12-B).
   Exactly one save button per width: below 768px the header copy and its
   save state are display: none, from 768px up the bar is. Visibility of a
   signed offer's bar runs through the hidden attribute alone (R5). */
@media (max-width: 767px) {
  /* Pitfall 1: sticky only sticks inside a block/flex parent — a grid item
     sticks within its own grid area, i.e. not at all. */
  .offer-view {
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
  }
  /* The one-column grids inside the mask: an implicit auto (or 1fr) track
     grows to the min-content of its widest item, and a collapsed card with a
     long title (white-space: nowrap) pushed the whole positions block to
     ~700px at 375px. minmax(0, 1fr) keeps every column at the screen width;
     the title then ends in its ellipsis. */
  .offer-view :is(.offer-block-body, .offer-cards, .offer-card-body, .offer-grid, .offer-subs, .offer-totals) {
    grid-template-columns: minmax(0, 1fr);
  }
  .page-header .offer-savestate,
  .page-header .offer-save-head,
  .page-header .offer-settings-head-actions {
    display: none;
  }
  /* "Abbrechen" of the master data is an anchor: a.btn-secondary is
     inline-flex without centring, a button centres its label by default. */
  .offer-savebar > a.btn-secondary {
    justify-content: center;
  }
  .offer-savebar .offer-savestate {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 12px;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* Position card head: the collapse switch takes what is left and its title
     ends in an ellipsis; when the collapsed title and meta need the room, the
     ↑ ↓ ✕ group wraps to its own line, right-aligned. Inside a collapsed
     switch the title keeps the first line and quantity · sum take one line of
     their own below it, instead of breaking word by word into a narrow
     column. */
  .offer-card-head {
    flex-wrap: wrap;
  }
  .offer-card-toggle {
    min-width: 0;
    flex: 1 1 auto;
    flex-wrap: wrap;
    row-gap: 0;
  }
  .offer-card-toggle > .offer-card-title {
    flex: 1 1 0;
  }
  .offer-card-toggle > .offer-meta {
    flex-basis: 100%;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .offer-sub .offer-card-actions {
    justify-content: flex-end;
    flex-wrap: wrap;
  }
}

@media (min-width: 768px) {
  .offer-savebar {
    display: none;
  }
}

/* F-8: 44 × 44 for the block heads, the icon buttons ↑ ↓ ✕ (42 px wide
   before) and the price switch — touch only, the mouse keeps its look. */
@media (pointer: coarse) {
  .offer-block-head {
    min-height: var(--control-h);
  }
  .offer-card-actions .btn-secondary,
  .offer-price-toggle {
    min-width: var(--control-h);
    min-height: var(--control-h);
  }
}

/* The target net (#offer-net) sits in .offer-total-row, not in .field, so the
   red frame of .field input[aria-invalid="true"] never reached it (12-02):
   same signal, same token. */
.offer-total-row input[aria-invalid="true"] {
  border-color: var(--danger);
}
/* ---- end 12-G ---- */

/* ---- 12-H Auswertung, settings, login ------------------------------------- */
/* D-03: the Auswertung shows everything it shows on the Mac; only the layout
   changes. No colour of its own here — the kind colours stay on the tiles. */
@media (max-width: 767px) {
  .ausw-modes {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .ausw-mode {
    min-height: var(--control-h);
  }

  /* F-3: the 7-column breakdown becomes one card per period — same table,
     one DOM tree. The caption of every number comes from its data-label,
     set from the header's own source in app/views/auswertung.js. */
  .ausw-table {
    background: none;
    border: 0;
    box-shadow: none;
  }

  .ausw-table thead {
    display: none;
  }

  .ausw-table,
  .ausw-table tbody {
    display: block;
  }

  .ausw-table tbody tr {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-xs) var(--space-md);
    margin-bottom: var(--space-sm);
    padding: var(--space-md);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-card);
  }

  /* Forces the kind pairs onto the line below "period … Gesamt n". */
  .ausw-table tbody tr::before {
    content: "";
    order: -1;
    flex-basis: 100%;
    height: 0;
  }

  .ausw-table tbody td {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-xs);
    padding: 0;
    border: 0;
    font-size: 14px;
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
  }

  .ausw-table tbody td.ausw-period {
    order: -3;
    flex: 1 1 auto;
    min-width: 0;
  }

  .ausw-table tbody td.ausw-total {
    order: -2;
  }

  .ausw-table tbody td[data-label]::before {
    content: attr(data-label);
    font-size: 12px;
    font-weight: var(--weight-regular);
    color: var(--text-muted);
  }

  /* Login card: margin to every edge, safe areas honoured (UI-SPEC § 7,
     § 8.12). The third max() term keeps the 400px card centred wherever the
     viewport is wider than card plus margins (a percentage margin resolves
     against the container width). */
  .auth-card {
    margin: calc(var(--space-3xl) + env(safe-area-inset-top)) max(var(--space-md), env(safe-area-inset-right), calc((100% - 400px) / 2)) var(--space-3xl) max(var(--space-md), env(safe-area-inset-left), calc((100% - 400px) / 2));
  }

  .auth-card :is(.btn-primary, .btn-secondary) {
    width: 100%;
    min-height: var(--control-h);
  }

  /* Settings (UI-SPEC § 8.12): when a passkey row wraps, the two buttons
     sit below name and meta on the right. */
  .passkey-actions {
    margin-left: auto;
  }
}

@media (pointer: coarse) {
  .passkey-actions .btn-text {
    min-height: var(--control-h);
  }
}
/* ---- end 12-H ---- */

/* ---- 12-I pull-to-refresh indicator --------------------------------------- */
/* Between top bar and content (UI-SPEC § 8.14). Zero height while idle, 48px
   while pulling, armed or loading; the icon turns 180deg when armed and gives
   way to the text alone while loading (no spinner, 01-UI-SPEC). The motion
   falls under the global prefers-reduced-motion rule. */
.ptr {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  height: 0;
  overflow: hidden;
  background: var(--bg);
  color: var(--text-muted);
  font-size: 14px;
  transition: height 160ms ease-out;
}

.ptr[data-state="pull"],
.ptr[data-state="armed"],
.ptr[data-state="loading"] {
  height: 48px;
}

.ptr .ptr-icon {
  transition: transform 160ms ease-out;
}

.ptr[data-state="armed"] .ptr-icon {
  transform: rotate(180deg);
}

.ptr[data-state="loading"] .ptr-icon {
  display: none;
}

.ptr-text {
  white-space: nowrap;
}

@media (min-width: 768px) {
  .ptr {
    display: none;
  }
}
/* ---- end 12-I ---- */
