

/* T.R.E.X. Finance — layout and components
 *
 * Built from the design project "T.R.E.X.", screens 2a (dashboard), 2b (login)
 * and 2c (CSV import). The design canvas expresses everything as inline styles
 * on a fixed 1440px artboard; here it becomes classes that survive a narrower
 * window, because a real browser is not an artboard.
 *
 * Three components come from the Motion UI Kit: Button, ProgressBar,
 * StaggerReveal. They are reimplemented in plain CSS and 60 lines of JS rather
 * than pulled in as a 190 KB React bundle — the kit's own source shows all
 * three are a handful of style rules and one IntersectionObserver, and a
 * server-rendered Blade app has no other reason to ship React.
 */
/* ---------- Base ---------- */*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--bg-alt);
  color: var(--fg-2);
  font-family: var(--font-body);
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--color-primary); }
a:hover { color: var(--color-heading-darken); }

h1, h2, h3, h4 { font-family: var(--font-display); color: var(--fg-1); margin: 0; }


/* Handwritten eyebrow — the live site's signature. */.eyebrow {
  font-family: var(--font-script);
  font-weight: var(--fw-semibold);
  font-size: 22px;
  line-height: 1.2;
  color: var(--color-primary);
}
.num   { font-variant-numeric: tabular-nums; }


/* Text that is matched literally — a search pattern, a purpose fragment. Marked
   by a frame rather than by a monospace face: the patterns here are names
   ("Vanessa Jacobsen", "BKK LINDE"), and setting a name in a code face makes it
   look like a machine identifier the reader must not touch. */.literal {
  background: var(--surface-2);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  padding: 1px 7px;
  white-space: nowrap;
}


/* ---------- Buttons (Motion UI Kit: core/Button.jsx) ----------
   Scoped to the legacy variants (.btn--*): only the login page and its
   x-ui.button component still wear them. A bare `.btn` rule here would
   override every BOOTSTRAP button on the migrated pages — that collision
   is exactly what the migration removed. */
.btn[class*="btn--"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  font-family: var(--font-display);
  font-weight: var(--fw-medium);
  border-radius: var(--radius-md);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: transform var(--duration-instant) var(--ease-out),
              background var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}
.btn[class*="btn--"]:active:not(:disabled) { transform: scale(0.97); }
.btn[class*="btn--"]:disabled { opacity: .4; cursor: not-allowed; }
.btn[class*="btn--"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.btn--sm { height: 32px; padding: 0 14px; font-size: var(--fs-sm); }
.btn--md { height: 40px; padding: 0 18px; font-size: var(--fs-base); }
.btn--lg { height: 48px; padding: 0 24px; font-size: var(--fs-base); }
.btn--block { width: 100%; }

.btn--primary { background: var(--accent); color: var(--on-accent); border: 1px solid transparent; }
.btn--primary:hover:not(:disabled) { background: var(--accent-hover); color: var(--on-accent); }

.btn--secondary { background: var(--surface-1); color: var(--text-1); border: 1px solid var(--border-1); }
.btn--secondary:hover:not(:disabled) { background: var(--surface-2); border-color: var(--border-2); color: var(--text-1); }

.btn--ghost { background: transparent; color: var(--text-1); border: 1px solid transparent; }
.btn--ghost:hover:not(:disabled) { background: var(--surface-2); color: var(--text-1); }


/* ---------- Progress bar (Motion UI Kit: core/ProgressBar.jsx) ---------- */.progress { flex: 1; height: 8px; background: var(--surface-3); border-radius: var(--radius-full); overflow: hidden; }
.progress__fill {
  display: block;
  height: 100%;
  border-radius: var(--radius-full);
  background: var(--accent);
  width: 0;
  transition: width var(--duration-slow) var(--ease-out);
}


/* ---------- Stagger reveal (Motion UI Kit: reveal/StaggerReveal.jsx) ----------
   The hidden start state is applied by JS, never by CSS alone. Without that,
   a failed script or a browser with JS off would leave the page permanently
   blank instead of merely unanimated. */.js-stagger > * {
  opacity: 0;
  transform: translateY(var(--stagger-travel, 16px));
  transition: opacity var(--duration-slow) var(--ease-out) var(--stagger-delay, 0s),
              transform var(--duration-slow) var(--ease-out) var(--stagger-delay, 0s);
}
.js-stagger.is-revealed > * { opacity: 1; transform: translateY(0); }


/* ---------- App shell ---------- */
/* One height for the two boxes at the top of the screen. The brand block used
   to be sized by its paddings (22 + 34 + 18) and came out eleven pixels taller
   than the topbar beside it, so the two rules met the vertical divider at
   different heights and the whole head of the page looked askew. Both read the
   same number now, and moving one moves the other.

   The bar's own content has to fit inside this: the topbar takes the number as
   a minimum, the brand block as a fixed height, so anything taller than 64 px
   in the bar pushes only one of the two down. The month picker was exactly
   that — see .topbar__chip select. */:root { --topbar-h: 64px; }

.shell { display: flex; min-height: 100vh; background: var(--bg-alt); }


/* Sticky, not scrolling away: the sign-out button lives at the foot of this
   column, and on a long page it was several screens below whatever the reader
   was looking at. Sticky rather than fixed keeps the main column's layout
   untouched — no left margin to keep in sync with the width. */.sidebar {
  width: 230px;
  flex: none;
  background: var(--bg);
  border-right: 1px solid var(--border-color);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
}
.sidebar__brand {
  height: var(--topbar-h);
  flex: none;
  display: flex;
  align-items: center;
  padding: 0 20px;
  border-bottom: 1px solid var(--border-color);
  color: var(--fg-1);
}
.sidebar__brand .logo { height: 26px; width: auto; display: block; }


/* The one part that may scroll. Nine entries fit; a tenth must not push the
   user block off the bottom of a fixed-height column, which is the only place
   the sign-out button exists. */.nav { display: flex; flex-direction: column; gap: 2px; padding: 14px 12px; overflow-y: auto; }
.nav__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  color: var(--fg-2);
  font: var(--fw-regular) var(--fs-sm) var(--font-display);
  text-decoration: none;
  transition: background var(--duration-micro) var(--ease-standard);
}
.nav__item:hover { background: var(--bg-alt); color: var(--fg-1); }
.nav__item[aria-current="page"] { background: var(--color-primary-light); color: var(--fg-1); font-weight: var(--fw-medium); }
.nav__item svg { flex: none; }

.sidebar__user {
  margin-top: auto;
  padding: 16px 20px;
  border-top: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  gap: 10px;
}
.avatar {
  width: 34px; height: 34px; flex: none;
  border-radius: 50%;
  background: var(--color-primary-darken);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  font: var(--fw-semibold) 13px var(--font-display);
}
.sidebar__user-name { font: var(--fw-medium) 13px var(--font-display); color: var(--fg-1); }
.sidebar__user-role { font: var(--fw-regular) 11px var(--font-body); color: var(--fg-3); }

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


/* Sticky like the sidebar beside it, and for the same reason: on a long table
   the heading is what says which page these figures belong to, and it was the
   first thing to scroll away.
   z-index above the sticky first and last columns of a matrix table (both at 1)
   — otherwise a pinned column slides over the bar instead of under it. The
   opaque background is not decoration: without it the rows show through. */.topbar {
  min-height: var(--topbar-h);
  flex: none;
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--bg);
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-4);
  /* 8 px, not 12: the tallest thing in this bar is a 40 px button, and at 12 it
     came to 64 plus a subpixel — one pixel past the brand block, which is the
     whole reason the two bottom rules drifted apart. The bar looks unchanged;
     min-height still sets the height and the row is centred inside it. */
  padding: var(--sp-2) var(--sp-6);
}
.topbar__title { font: var(--fw-semibold) 18px var(--font-display); color: var(--fg-1); }
.topbar__chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  padding: 7px 12px;
  font: var(--fw-medium) 13px var(--font-display);
  color: var(--fg-1);
  background: var(--bg);
  cursor: pointer;
}

/* The month picker sits INSIDE the chip, which already draws the border and
   the padding. The select must contribute nothing of its own — it was picking
   up the standalone-control style (7 px top and bottom plus a border) on top of
   the chip's own, came out 48 px tall and pushed the whole topbar past the
   brand block beside it. Was three inline styles on three pages that reset
   border, background and font and forgot the padding; one rule instead.

   The :not() is carried along, not decoration: the standalone-control rule is
   select:not(.field select) and outweighs a plain .topbar__chip select, so
   without it the padding this rule exists to remove stays. */.topbar__chip select:not(.field select) {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.topbar__stamp { font: var(--fw-regular) var(--fs-xs) var(--font-body); color: var(--fg-3); }


/* The reporting-period switch. One control that states which period every
   period-bound figure on the page describes — links, not JS, so the period
   survives a reload and can be sent around as a URL. */.seg {
  display: inline-flex;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg);
}
.seg__item {
  padding: 7px 14px;
  font: var(--fw-medium) 13px var(--font-display);
  color: var(--fg-2);
  text-decoration: none;
  border-left: 1px solid var(--border-color);
}
.seg__item:first-child { border-left: 0; }
.seg__item:hover { background: var(--bg-alt); color: var(--fg-1); }
.seg__item[aria-current="true"] {
  background: var(--color-primary-light);
  color: var(--color-heading-darken);
}
.topbar__actions { margin-left: auto; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }

.content { padding: var(--sp-6); }


/* Pushes the meta line to the bottom so the big figures line up across tiles. */


/* ---------- Bento ----------
   The middle of the overview. Two-column rows sized every card by its
   neighbour: the thirteen-line cost list stretched its row and left a
   card-sized hole under the chart. Explicit spans let the tall card run down
   the right-hand side while the short ones stack beside it — no card is sized
   by the accident of what sits next to it. */.bento {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--sp-5);
  padding-bottom: var(--sp-5);
}
.bento > .card { min-width: 0; }
.bento__breit  { grid-column: 1 / 8; }
.bento__schmal { grid-column: 8 / 13; }
.bento__hoch   { grid-column: 8 / 13; grid-row: span 2; }


/* Below this width the spans would squeeze both columns unreadable; the cards
   simply stack in source order. */@media (max-width: 1100px) {
  .bento__breit, .bento__schmal, .bento__hoch { grid-column: 1 / -1; grid-row: auto; }
}


/* ---------- Sortable columns ----------
   A sortable header says so before it is clicked: the caret sits there greyed
   out and only darkens on the active column. A column that reveals its
   sortability on hover is one nobody discovers. */.sortierbar { cursor: pointer; user-select: none; white-space: nowrap; }
.sortierbar:hover { color: var(--fg-1); }
.sortierbar::after {
  content: '↕';
  margin-left: 5px;
  opacity: .35;
  font-size: 10px;
}
.sortierbar[aria-sort="ascending"]::after  { content: '↑'; opacity: 1; color: var(--color-primary); }
.sortierbar[aria-sort="descending"]::after { content: '↓'; opacity: 1; color: var(--color-primary); }


/* ---------- Expandable table rows ----------
   The object table answers "which object earns"; the row beneath it answers
   "on which tickets", which is the question that follows every bad margin.
   Rendered with the page and collapsed by script — see the note in the view. */.aufklapp {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
}
.aufklapp:hover { color: var(--color-primary); }
.aufklapp__pfeil {
  display: inline-block;
  width: 12px;
  color: var(--fg-3);
  transition: transform var(--duration-micro) var(--ease-standard);
}
.aufklapp[aria-expanded="true"] .aufklapp__pfeil { transform: rotate(90deg); }
.aufklapp[aria-expanded="true"] { font-weight: var(--fw-medium); }


/* The detail sits INSIDE the row it belongs to, indented and on a tinted
   ground, so an open row never reads as more top-level rows. */.detailzeile > td {
  padding: 0 0 0 var(--sp-6);
  background: var(--surface-2);
}
.table .detail { margin: var(--sp-3) 0; background: transparent; }
.table .detail th {
  font-size: 10px;
  color: var(--fg-3);
  padding-top: 0;
}
.table .detail td, .table .detail th { border-color: rgba(5, 33, 63, .07); }
.table .detail tr:last-child td { border-bottom: 0; }


/* ---------- Margin against the target ----------
   Every margin in the cockpit is judged against one line (ObjectService::
   MARGE_ZIEL). Colour rather than a chip: these sit in dense number columns,
   and a badge per row would turn a table into a traffic light. */.marge--gut     { color: var(--status-ok-ink); font-weight: var(--fw-medium); }
.marge--schwach { color: var(--status-fault-ink); font-weight: var(--fw-medium); }


/* The colour must survive the base colours of the cells and value spans it
   sits in (.table td, .cost-row__value, .kpi__value all set --fg-1 with
   higher or equal specificity) — without these, a loss renders in the same
   ink as every other number, which is exactly the reading error the class
   exists to prevent. */.table td.marge--gut, .cost-row__value.marge--gut, .kpi__value.marge--gut { color: var(--status-ok-ink); }
.table td.marge--schwach, .cost-row__value.marge--schwach, .kpi__value.marge--schwach { color: var(--status-fault-ink); }


/* ---------- Order list ----------
   The subcontractor orders behind a ticket. A table inside a table read as a
   second dataset competing with the first, so this stayed a list — but one
   line per order forced everything onto a single row that ran out of width and
   dropped the interesting part. Each order is a small card now: who and what on
   the left, budget against settlement on the right, where the eye goes anyway.

   The left column is `minmax(0, 1fr)` rather than `1fr`: a grid track sized 1fr
   refuses to shrink below its content, and a long order title would push the
   figures off the card instead of ellipsising. */.auftragsliste {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: var(--sp-3) 0 var(--sp-2);
}
.auftrag {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--sp-5);
  padding: 12px 16px;
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--border-color);
}
.auftrag--abgelehnt { opacity: .6; }

.auftrag__kopf {
  grid-column: 1;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
}
.auftrag__punkt {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--fg-3);
}
.auftrag--fertig .auftrag__punkt { background: var(--status-ok-ink); }
.auftrag--offen .auftrag__punkt  { background: var(--status-warn-ink); }

/* Wraps, never truncates — the cockpit-wide no-clipping rule. */.auftrag__wer {
  font: var(--fw-semibold) var(--fs-sm) var(--font-display);
  color: var(--fg-1);
  min-width: 0;
  overflow-wrap: anywhere;
}
.auftrag__art {
  font-size: var(--fs-xs);
  color: var(--fg-2);
}
.auftrag__was {
  grid-column: 1;
  margin: 3px 0 0;
  font-size: var(--fs-xs);
  color: var(--fg-1);
  overflow-wrap: anywhere;
}

/* Facts separated by a rule drawn in CSS, so no separator survives a field
   that turned out to be empty — 46 % of orders carry no trade, 28 % no place. */.auftrag__meta {
  grid-column: 1;
  margin: 2px 0 0;
  font-size: var(--fs-xs);
  color: var(--fg-3);
  display: flex;
  flex-wrap: wrap;
  gap: 0 14px;
  min-width: 0;
}
.auftrag__meta > span { position: relative; }
.auftrag__meta > span + span::before {
  content: "";
  position: absolute;
  left: -8px; top: .3em; bottom: .25em;
  border-left: 1px solid var(--border-color);
}
.auftrag__nr { font-variant-numeric: tabular-nums; }


/* Budget against settlement, as a three-cell strip.
   Fixed cell width and tabular figures, so six orders under one ticket line up
   as columns without being a table. Every cell carries all three lines — label,
   figure, qualifier — even when the qualifier is only "keins vereinbart": a
   blank line would let the figures of the cells drift out of alignment, and the
   whole point of the strip is that they do not. */.auftrag__zahlen {
  grid-column: 2;
  grid-row: 1 / span 3;
  display: flex;
  margin: 0;
  text-align: right;
}
.kennzahl {
  min-width: 108px;
  padding: 0 14px;
  border-left: 1px solid var(--border-color);
}
.kennzahl:first-child { border-left: 0; }
.kennzahl:last-child  { padding-right: 0; }
.kennzahl dt {
  font: var(--fw-medium) 10px var(--font-body);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg-3);
}
.kennzahl dd {
  margin: 3px 0 0;
  font: var(--fw-semibold) 15px var(--font-display);
  color: var(--fg-1);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  white-space: nowrap;
}
.kennzahl__zusatz {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  line-height: 1.3;
  color: var(--fg-3);
  white-space: nowrap;
}

/* Deliberately no red and no green on the gap between release and settlement.
   The release is what the property manager approves before anyone has seen the
   work; the invoice is the correct figure. Colouring the difference would mark
   139 correct invoices as a fault — and colour is read before the number, so
   the false claim lands first. Margin against MARGE_ZIEL is coloured, this is
   not: one is a verdict the data supports, the other is not. */.kennzahl--leer dd, .kennzahl--leer .kennzahl__zusatz { color: var(--fg-3); font-weight: var(--fw-regular); }

.auftrag__bilanz {
  margin: 4px 0 0;
  padding: 0 14px;
  font-size: var(--fs-xs);
  color: var(--fg-2);
}

@media (max-width: 900px) {
  .auftrag { grid-template-columns: minmax(0, 1fr); }
  .auftrag__zahlen {
    grid-column: 1;
    grid-row: auto;
    margin-top: 8px;
    text-align: left;
    justify-content: space-between;
    gap: var(--sp-3);
  }
}


/* ---------- Charts: the house style ----------
   Binding, from the Chart-Galerie design template (Claude Design project
   T.R.E.X., 24.08.2026). Every chart in the cockpit uses these: the brand
   gradient for filled series, a draw-on for lines, a rise-up for bars, and
   the standard easing. Flat single-colour fills and instant appearance are
   no longer the house look.

   Motion is decoration with a job — it shows the reading direction of a
   series. Where the reader asked for less of it, all of it goes. */
@keyframes chartZeichnen { to { stroke-dashoffset: 0; } }
@keyframes chartEinblenden { to { opacity: 1; } }
@keyframes chartAufsteigen { to { transform: scaleY(1); } }
@keyframes chartPuls {
  0%   { transform: scale(1); opacity: .5; }
  100% { transform: scale(2.6); opacity: 0; }
}

/* A line draws itself along its own length. pathLength="1" on the element
   makes the dash arithmetic independent of the actual path length. */
.chart-linie {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: chartZeichnen 1.4s var(--ease-standard) .2s forwards;
}
.chart-flaeche {
  opacity: 0;
  animation: chartEinblenden .8s var(--ease-standard) .6s forwards;
}
/* Bars grow out of the baseline; each sets its own transform-origin to the
   baseline y, because SVG has no percentage origin per shape. */
.chart-balken {
  transform: scaleY(0);
  animation: chartAufsteigen .7s var(--ease-standard) forwards;
}
.chart-puls {
  animation: chartPuls 2s var(--ease-standard) 1.8s infinite;
}
/* The dot that marks "this is where the series ends today". */
.chart-jetzt {
  opacity: 0;
  animation: chartEinblenden .4s ease 1.7s forwards;
}

@media (prefers-reduced-motion: reduce) {
  .chart-linie, .chart-flaeche, .chart-balken, .chart-puls, .chart-jetzt {
    animation: none !important;
    opacity: 1 !important;
    stroke-dashoffset: 0 !important;
    transform: none !important;
  }
}

/* ---------- Chart tooltip ----------
   Fed entirely from data attributes the server formatted; the script only
   copies text. pointer-events: none, or the tooltip would sit between the
   cursor and the hover zone that opened it and flicker. */.charttip {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  min-width: 200px;
  background: var(--bg);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-2);
  padding: 10px 12px;
  font: var(--fw-regular) var(--fs-xs) var(--font-body);
}
.charttip__titel {
  font: var(--fw-semibold) var(--fs-sm) var(--font-display);
  color: var(--fg-1);
  margin-bottom: 6px;
}
.charttip__zeile {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 2px 0;
  color: var(--fg-2);
  white-space: nowrap;
}
.charttip__zeile b { color: var(--fg-1); font-variant-numeric: tabular-nums; }
.charttip__zeile--summe {
  margin-top: 5px;
  padding-top: 6px;
  border-top: 1px solid var(--border-color);
}
.charttip__punkt {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  vertical-align: 0;
}
.charttip__hinweis {
  margin-top: 6px;
  color: var(--fg-3);
  white-space: nowrap;
}


.up   { color: var(--status-ok-ink); }
.down { color: var(--status-fault-ink); }


/* ---------- Chips ---------- */


/* ---------- Cost rows ---------- */.cost-row { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; align-items: baseline; padding-bottom: 14px; }
.cost-row__label { font: var(--fw-regular) 13px var(--font-body); color: var(--fg-1); }

/* The grid column already bounds the label at the card's real width — a ch
   cap on top of it folded names while half the card sat empty (Dave,
   12.08.). Outranks the .clip widths. */.cost-row .cost-row__label { max-width: none; }
.cost-row__value { font: var(--fw-semibold) 13px var(--font-display); color: var(--fg-1); font-variant-numeric: tabular-nums; }
.cost-row__bar { grid-column: 1 / -1; display: flex; align-items: center; gap: 10px; }


/* ---------- Tables ----------
   Base table styling belongs to BOOTSTRAP now (+ theme.css for the TREX
   voice: uppercase thead, tabular figures via .num). This file keeps only
   what Bootstrap has no word for: the matrix stickiness, the mobile card
   transform, and transitional helpers below (delete once no view uses
   them any more). */.table { font-size: var(--fs-sm); }
/* Cut-through rows (averages, per-month splits) speak quietly inside a
   foot — Bootstrap has no word for a de-emphasised tfoot row. */
.table .schnittzeile td { color: var(--fg-3); font-size: var(--fs-xs); }


/* The clip widths WRAP, they no longer truncate (Dave, 12.08.2026: "hier
   wird auch wieder abgeschnitten der text, das will ich grundsätzlich
   nicht"). The max-widths stay — they are what makes a marathon name fold
   to a second line instead of stretching its column — but the ellipsis is
   gone everywhere: nothing in this cockpit cuts a name or a title off.
   Not scoped to .table: the cost rows on the overview use them too. */.clip, .clip--wide, .clip--titel {
  white-space: normal;
  /* break-word, not anywhere: `anywhere` drops the cell's min-content width
     to a single character, the table then starves the column and "Fischer
     Energietechnik" arrives as "Energietechni k". break-word keeps the
     longest word as the minimum — the column claims its space and only a
     word longer than the whole column breaks. */
  overflow-wrap: break-word;
}
.clip { max-width: 22ch; }
.clip--wide { max-width: 34ch; }
.clip--titel { max-width: 52ch; }


/* Every cell starts at the same height. Rows mixing one- and two-line cells
   (an id with its date under it beside a plain title) aligned each cell to
   its own middle, and the row read as scattered. */.table th, .table td { vertical-align: top; }


/* Wide month matrices: the row label has to stay put while the months scroll,
   or a figure halfway along the row belongs to nothing the reader can see.
   The background is repeated on the sticky cell because a transparent one
   would let the scrolling columns show through underneath it. */.table--matrix th:first-child,
.table--matrix td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
  box-shadow: 1px 0 0 var(--border-color);
}
.table--matrix th { background: var(--surface); }


/* The row total pinned to the right edge for the same reason as the label on
   the left: a twelve-month matrix is wider than any screen, and the one figure
   the reader wants after scanning a row is its total. Scrolling to find it
   defeats the point of having it. */.table--matrix th:last-child,
.table--matrix td:last-child {
  position: sticky;
  right: 0;
  z-index: 1;
  background: var(--surface);
  box-shadow: -1px 0 0 var(--border-color);
  /* Room on both sides: the cell has a rule down its left edge, and a figure
     sitting on that rule reads as if it were cut off. */
  padding-left: var(--sp-5);
  padding-right: var(--sp-2);
}


/* The same rule on the other side of the pinned first column. Without it the
   second column's text starts flush against the divider — "Peter Schweigl
   GmbH" touching the line reads as if the column were cut off rather than
   simply next. */.table--matrix th:first-child + th,
.table--matrix td:first-child + td { padding-left: var(--sp-5); }


/* ---------- Wide tables on a phone ----------
   Two pinned columns are a desktop idea. At 375px they take the label on the
   left and the total on the right and leave about 120px in the middle, so the
   figures that were the point of the table arrive as "218 €" with the rest cut
   off — and the header above them as "UF" and "DECKL".
   The two shapes of table need two different answers, so they get them. */@media (max-width: 800px) {
  
  /* A twelve-month matrix cannot become anything but a scrolling table, so it
     stays one — just without spending half the screen on pinned columns. */.table--matrix th:first-child,
  .table--matrix td:first-child,
  .table--matrix th:last-child,
  .table--matrix td:last-child {
    position: static;
    box-shadow: none;
  }
  .table--matrix th:first-child + th,
  .table--matrix td:first-child + td { padding-left: var(--sp-3); }
  .table--matrix th:last-child,
  .table--matrix td:last-child { padding-left: var(--sp-3); }

  

  
  /* The short tables — objects, suppliers, staff — become cards. Seven columns
     of numbers do not fit across a phone at any font size worth reading, and a
     row that has to be scrolled sideways to be understood is not a row.
     Only applied once the script has labelled the cells: without the labels
     these blocks would be a column of bare numbers, which is worse than a
     table that scrolls. */.table--karten.hat-labels,
  .table--karten.hat-labels tbody,
  .table--karten.hat-labels tr,
  .table--karten.hat-labels td { display: block; }

  .table--karten.hat-labels thead { display: none; }

  .table--karten.hat-labels tr {
    background: var(--surface);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-sm);
    padding: var(--sp-3) var(--sp-4);
    margin-bottom: var(--sp-3);
  }
  .table--karten.hat-labels tr:last-child { margin-bottom: 0; }

  
  /* Label above its value, not beside it (Dave, 19.08.).
     Side by side, the label eats roughly half a phone's card width and the
     value gets what is left — "Raumlufttechnische Anlagen" then broke
     mid-word, and a value long enough could not shrink at all and ran off
     the right edge. Stacked, every value has the full card: it breaks
     between words, the labels form one clean left column, and no cell can
     overflow. Costs a little vertical space and is worth it. */.table--karten.hat-labels td {
    display: block;
    padding: 5px 0;
    border-bottom: 0;
    text-align: left;
    overflow-wrap: break-word;
  }

  
  /* The label the header row used to carry, now above its own value.
     text-align is set explicitly: most of these cells carry .right for their
     figure, and the label would inherit it and sit off its own left edge — the
     column of labels then reads as ragged for no reason anybody could see. */.table--karten.hat-labels td::before {
    content: attr(data-label);
    display: block;
    margin-bottom: 1px;
    text-align: left;
    font: var(--fw-medium) 11px var(--font-body);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--fg-2);
  }

  
  /* The first cell is the name of the thing and carries the row: full width,
     no label in front of it, and set apart as a heading. */.table--karten.hat-labels td:first-child {
    display: block;
    font: var(--fw-medium) var(--fs-base) var(--font-display);
    padding: 0 0 var(--sp-2);
    margin-bottom: var(--sp-2);
    border-bottom: 1px solid var(--border-color);
    max-width: none;
  }
  .table--karten.hat-labels td:first-child::before { content: none; }

  
  /* Figures line up with everything else in the card. .right exists for the
     table's number column; in a card there is no column, and a figure pushed
     to the right edge while every other value starts at the left reads as a
     stray. Only the tabular figures and the no-wrap from .table .right still
     earn their keep here. */.table--karten.hat-labels td.right { text-align: left; }

  
  /* A matrix table indents the cell after the row label to set the figures
     apart from it. In card form there is no row label beside them and no
     column to set apart, so the indent just pushes one value out of the left
     edge every other value shares. Needs this much selector because
     :first-child counts as a class and outranks the plain card rule. */.table--karten.hat-labels.table--matrix td:first-child + td,
  .table--karten.hat-labels.table--matrix td:last-child { padding-left: 0; }

  
  /* A cell that holds an input is a form field, not a figure: the label goes
     above and the control takes the full width. Side by side, a name field on a
     375px screen is about eleven characters wide, which is where "Dave Böhme"
     arrived as "Dave Böhn" and looked like stored data had been lost. */.table--karten.hat-labels td.hat-feld { display: block; text-align: left; }
  .table--karten.hat-labels td.hat-feld::before { display: block; margin-bottom: 4px; }
  .table--karten.hat-labels td.hat-feld input,
  .table--karten.hat-labels td.hat-feld select,
  .table--karten.hat-labels td.hat-feld textarea {
    width: 100%;
    max-width: 100%;
    /* Overrides the fixed widths the desktop table sets on single controls. */
    min-width: 0;
    /* 16px, or iOS zooms the page in on focus and leaves it zoomed. */
    font-size: 16px;
    padding: 10px 12px;
    /* Stated, not hoped for: padding plus line-height came to 41px, and the
       floor for something a finger has to hit is 44. */
    min-height: 44px;
  }

  
  /* A cell the row had nothing for would otherwise show its label over an empty
     space, which reads as missing data rather than as not applicable. */.table--karten.hat-labels td:empty { display: none; }

  
  /* The row's own action — "Speichern", "Löschen". In the dense desktop table
     it is a 32px small button; on a card it is the thing the finger is aiming
     for, and 32px is below the floor. */.table--karten.hat-labels td .btn {
    min-height: 44px;
    padding-top: 0;
    padding-bottom: 0;
  }

  
  /* Detail rows that the sort script folds out sit inside the card rather than
     pretending to be one. */.table--karten.hat-labels tr[hidden] { display: none; }
}


/* ---------- Forms ---------- */.field { display: flex; flex-direction: column; gap: 6px; padding-bottom: 18px; }
.field label { font: var(--fw-medium) 11px var(--font-body); letter-spacing: .1em; text-transform: uppercase; color: var(--fg-2); }

/* max-width, not width: a select is as wide as its longest option unless it is
   told otherwise, and the import mask offers "Standard (Buchung, Valuta,
   Zahler, Zweck, Zahlungsart, Betrag)" — sixty characters that pushed the whole
   page wider than the phone and left it scrolled sideways with the logo off the
   left edge. The field keeps its natural width everywhere it fits. */.field input, .field select {
  max-width: 100%;
  min-width: 0;
  font: var(--fw-regular) var(--fs-base) var(--font-body);
  color: var(--fg-1);
  background: var(--surface);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  transition: border-color var(--duration-micro) var(--ease-standard),
              box-shadow var(--duration-micro) var(--ease-standard);
}
.field input:hover { border-color: var(--color-gray-darken); }
.field input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(21, 157, 169, .18);
}
.field--error input { border-color: var(--status-fault); }


/* Fields side by side instead of a column of full-width boxes. A name and a
   date do not need 1.500 pixels each, and stacked they push the submit button
   below the fold of a card. Collapses to one column when there is no room. */.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0 var(--sp-4);
  max-width: 900px;
}


/* A checkbox reads left to right with its label beside it, not above it in
   small caps — .field's column layout turns "Zahlt Lohnsteuer" into a heading
   for a control that is one line high. */.field--check {
  flex-direction: row;
  align-items: center;
  gap: 10px;
  padding-bottom: 18px;
}
.field--check label {
  font: var(--fw-regular) var(--fs-sm) var(--font-body);
  letter-spacing: normal;
  text-transform: none;
  color: var(--fg-1);
  cursor: pointer;
}


/* Inputs inside a data table fill their cell: a fixed character width cut
   "Vanessa Jacobsen" to "Vanessa Jaco" and looked like stored data was lost. */.table input[type="text"],
.table input[type="date"],
.table select { width: 100%; min-width: 11ch; }


/* Standalone controls — the assignment queue puts a select inside a table cell
   and a bulk bar, where .field's column layout and 18px of bottom padding would
   push every row apart. Same visual language, no layout of its own.

   Date and month are listed explicitly. Left out, the settings table rendered
   its entry and exit fields in the browser's own style — a different font, a
   different height and no border — right next to text fields that carried the
   house style. One row, two design languages. */select:not(.field select),
input[type="text"]:not(.field input),
input[type="search"]:not(.field input),
input[type="date"]:not(.field input),
input[type="month"]:not(.field input),
input[type="number"]:not(.field input) {
  font: var(--fw-regular) var(--fs-sm) var(--font-body);
  color: var(--fg-1);
  background: var(--surface);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  padding: 7px 10px;
  max-width: 100%;
}
select:not(.field select):focus,
input[type="text"]:not(.field input):focus,
input[type="search"]:not(.field input):focus,
input[type="date"]:not(.field input):focus,
input[type="month"]:not(.field input):focus,
input[type="number"]:not(.field input):focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(21, 157, 169, .18);
}


/* The little calendar button sits inside the field and inherits the browser's
   blue otherwise. */input[type="date"]::-webkit-calendar-picker-indicator,
input[type="month"]::-webkit-calendar-picker-indicator {
  opacity: .5;
  cursor: pointer;
}
input[type="date"]:hover::-webkit-calendar-picker-indicator,
input[type="month"]:hover::-webkit-calendar-picker-indicator { opacity: 1; }


/* Checkboxes in the house colour rather than the system blue, and large enough
   to hit without aiming. */input[type="checkbox"] {
  width: 17px;
  height: 17px;
  accent-color: var(--color-primary);
  cursor: pointer;
  vertical-align: middle;
}


/* The action column of the assignment queue. Fixed width so the category names
   ("Fremdleistungen / Handwerker") do not stretch the table until the column a
   person actually clicks is pushed off the right edge. */.table select.einzeln { width: 190px; }


/* A button that carries no weight of its own: it sits next to a primary action
   and must not compete with it. */.linkish {
  background: none;
  border: 0;
  padding: 0;
  font: var(--fw-regular) var(--fs-sm) var(--font-body);
  color: var(--color-primary);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.linkish:hover { color: var(--color-heading-darken); }

.alert {
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  margin-bottom: 18px;
}
.alert--error { background: var(--status-fault-bg); color: var(--status-fault-ink); }
.alert--info  { background: var(--color-primary-light); color: var(--color-heading-darken); }


/* ---------- Auth (screen 2b) ---------- */.auth { display: flex; min-height: 100vh; background: var(--bg); }

.auth__brand {
  width: 44%;
  flex: none;
  background: var(--color-primary-darken);
  color: #fff;
  display: flex;
  flex-direction: column;
  padding: 44px 48px;
  position: relative;
  overflow: hidden;
}

/* The gradient hairline along the bottom edge — the logo's own teal ramp. */.auth__brand::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 0; height: 5px;
  background: var(--grad-logo);
}
.auth__brand .logo { width: 190px; height: auto; }
.auth__claim {
  font-family: var(--font-script);
  font-weight: var(--fw-semibold);
  font-size: 30px;
  line-height: 1.2;
  color: var(--color-secondary);
}
.auth__copy { font: var(--fw-regular) var(--fs-sm)/var(--lh-body) var(--font-body); color: var(--color-gray-darken); margin-top: var(--sp-4); max-width: 340px; }
.auth__triptych { display: flex; gap: 18px; margin-top: 28px; font-family: var(--font-script); font-weight: var(--fw-semibold); font-size: 22px; color: var(--color-secondary); }
.auth__triptych span[aria-hidden] { color: rgba(255, 255, 255, .35); }

.auth__form { flex: 1; display: flex; align-items: center; justify-content: center; padding: var(--sp-8) var(--sp-6); }
.auth__form-inner { width: min(380px, 100%); }
.auth__title { font: var(--fw-bold) 30px var(--font-display); color: var(--fg-1); margin: 2px 0 6px; }
.auth__sub { font: var(--fw-regular) var(--fs-sm) var(--font-body); color: var(--fg-3); }


/* The navy panel is decoration. Below 900px it costs more than it says, so it
   goes and the form takes the whole width. */@media (max-width: 900px) {
  .auth { flex-direction: column; }
  .auth__brand { display: none; }
}


/* ---------- Drawer trigger and scrim ----------
   Both exist only below 800px; on a wide screen the sidebar is simply there and
   neither has anything to do. */.burger { display: none; }
.scrim { display: none; }


/* ---------- Narrow screens ----------
   The sidebar was a horizontally scrolling strip of eleven entries here, of
   which three were visible and nothing said the other eight existed. It is a
   drawer now: off-canvas, opened by the button in the bar, closed by the scrim,
   Escape, or simply following a link. */@media (max-width: 800px) {
  .shell { flex-direction: column; }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    /* Never the full width: the strip of page left visible is what says the
       drawer is a layer over the page rather than a new one. */
    width: min(284px, 85vw);
    height: 100%;
    height: 100dvh;
    z-index: 40;
    border-right: 1px solid var(--border-color);
    border-bottom: 0;
    transform: translateX(-100%);
    /* Out of the reading order while closed. Without this the eleven entries
       stay tabbable off-screen and the first Tab press on a page appears to do
       nothing eight times in a row. */
    visibility: hidden;
    /* The visibility delay only applies in this direction — closing. It holds
       the panel visible for the length of the slide, so the closing animation
       can be seen instead of the drawer blinking out on the first frame.
       Written as a plain transition of visibility it would apply in BOTH
       directions, and opening would then leave the drawer hidden for half the
       duration: long enough that focus() lands on nothing and the keyboard
       stays behind the scrim. Measured — it is why the open state below resets
       the delay to zero. */
    transition: transform 240ms var(--ease-standard), visibility 0s linear 240ms;
  }
  .sidebar.is-offen {
    transform: translateX(0);
    visibility: visible;
    box-shadow: var(--shadow-2);
    /* Visible from the first frame: focus moves in here as the drawer opens. */
    transition: transform 240ms var(--ease-standard), visibility 0s linear 0s;
  }

  
  /* Roomier than on the desktop rail: 44px is the floor for a touch target and
     10px of padding on a 20px line does not reach it. */.nav { flex-direction: column; padding: 10px 10px; gap: 4px; }
  .nav__item { padding: 12px 14px; min-height: 44px; }

  .sidebar__user { margin-top: auto; }

  .scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 30;
    background: rgba(15, 23, 32, .5);
    opacity: 0;
    visibility: hidden;
    transition: opacity 240ms var(--ease-standard), visibility 240ms;
  }
  .scrim.is-offen { opacity: 1; visibility: visible; }

  
  /* Exit faster than enter, both parts in step. The visibility delay has to
     come down with it, or the drawer would stay visible for 60ms after it has
     finished sliding out. */.sidebar:not(.is-offen) {
    transition: transform 180ms var(--ease-standard), visibility 0s linear 180ms;
  }
  .scrim:not(.is-offen) { transition-duration: 180ms; }

  .burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    flex: none;
    margin-left: calc(var(--sp-2) * -1);
    border: 0;
    background: none;
    border-radius: var(--radius-sm);
    color: var(--fg-1);
    cursor: pointer;
    transition: background var(--duration-micro) var(--ease-standard);
  }
  .burger:hover { background: var(--bg-alt); }
  .burger:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }

  
  /* The title has to survive next to the button on a 375px screen without
     pushing the row into a second line. */.topbar { gap: var(--sp-3); padding-left: var(--sp-4); padding-right: var(--sp-4); }
  .topbar__title { font-size: 16px; min-width: 0; overflow-wrap: anywhere; }

  .content { padding: var(--sp-4); }

  
  /* While the drawer is open the page behind it must not scroll away under the
     finger. */body.hat-drawer { overflow: hidden; }
}


/* The drawer is a phone affordance. Resizing a desktop window past the
   breakpoint while it happens to be marked open must not lock the page. */@media (min-width: 801px) {
  body.hat-drawer { overflow: visible; }
}


/* ---------- Reduced motion ----------
   Everything lands on its end state at once. Nothing that only exists as an
   animation may become invisible here. */@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-delay: 0ms !important;
    transition-duration: .001ms !important;
    transition-delay: 0ms !important;
  }
  .js-stagger > * { opacity: 1; transform: none; }
}


/* ---------- Skeleton while the next page renders ----------
   These pages are rendered on the server against the live TREX schema, and a
   click costs between 100 ms and a second. Until the response arrives the
   browser keeps showing the PREVIOUS page with its figures intact, and nothing
   on screen says so. The skeleton is drawn into the cards that are already
   there — same boxes, same heights — so the layout does not jump on the way in
   or on the way out. */.skel { overflow: hidden; }


/* The bars live in a wrapper of their own: the cards they replace do not share
   a layout — two of the four KPI tiles are display:block, the chart card
   centres its children — so a skeleton built on the card would come out stacked
   in one place and centred in the next. */.skel__inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 10px;
  width: 100%;
}
.skel__bar {
  flex: none;
  border-radius: 4px;
  /* The sweep needs real contrast against its own base. The first version
     ran surface-2 (#f0f2f5) against bg-alt (#f9f8f8) — nine steps per channel,
     an animation nobody could see. surface-3 against white is a difference of
     twenty-five and reads as movement across the room. */
  background: linear-gradient(90deg, var(--surface-3) 20%, #fbfcfd 40%, var(--surface-3) 60%);
  background-size: 220% 100%;
  animation: skelzug 1.25s ease-in-out infinite;
}
.skel__bar--gross { border-radius: 5px; }

@keyframes skelzug {
  0%   { background-position: 100% 0; }
  100% { background-position: 0 0; }
}


/* Reduced motion keeps the shape and drops the sweep — a still grey block is
   the honest version of the same message. */@media (prefers-reduced-motion: reduce) {
  .skel__bar { animation: none; background: var(--surface-2); }
}


/* ---------- Trades decision UI (stage 4) ----------
   The make-or-buy page: assumption forms, grouped leaf table, chart
   legends, card footnotes. Classes instead of inline styles — one place
   to tune, every page reads the same. */
/* Root label flush left, its leaves indented — hierarchy reads outside-in. */.table td.gewerk-gruppe {
  padding-left: 0;
  padding-top: var(--sp-4);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-xs);
  color: var(--fg-3);
  text-transform: uppercase;
  letter-spacing: .04em;
}
.table td.gewerk-leaf { padding-left: 22px; }


/* Assumption forms: a labelled row of inputs, never a settings page. */.annahmen { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 0 0 var(--sp-4); font-size: var(--fs-xs); }
.annahmen--stapel { align-items: flex-end; }
.annahmen__feld { display: flex; flex-direction: column; gap: 2px; }
.annahmen__knopf { border: 1px solid var(--border); border-radius: 6px; }
.feld-geld { width: 100px; }
.feld-prozent { width: 60px; }


/* Chart legends: a coloured swatch or line stub plus its word. */.legende { display: flex; gap: 16px; flex-wrap: wrap; margin: 0 0 var(--sp-3); font-size: var(--fs-xs); color: var(--fg-2); }
.legende__punkt { display: flex; align-items: center; gap: 6px; }
.legende__farbe { width: 14px; height: 8px; border-radius: 2px; }
.legende__linie { width: 14px; height: 0; border-top: 2px solid currentColor; }


.karten-text { font-size: var(--fs-xs); margin: 0 0 var(--sp-4); }
.klein { font-size: var(--fs-xs); }


/* Two thin bars stacked inside one cost row (revenue over cost). */.cost-row__doppel { flex: 1; display: grid; gap: 3px; }


/* ---------- Reporting-period picker ---------- */
/* One dropdown instead of the old segment-plus-month-select pair: quick picks
   on the left (plain GET links, so a period survives a reload and travels as
   a URL), a month-range grid on the right, Abbrechen/Anwenden below — the
   pattern the main TREX date picker uses. A MONTH grid, not a day calendar:
   every figure in this cockpit is month-grained, and a picker offering days
   would promise a precision the queries do not have. */.zeitwahl { position: relative; }
.zeitwahl__knopf {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  padding: 7px 12px;
  font: var(--fw-medium) 13px var(--font-display);
  color: var(--fg-1);
  background: var(--bg);
  cursor: pointer;
  user-select: none;
  list-style: none; /* the default disclosure triangle of a <summary> */
}
.zeitwahl__knopf::-webkit-details-marker { display: none; }
.zeitwahl__knopf:hover { background: var(--bg-alt); }
.zeitwahl__pfeil { color: var(--fg-3); font-size: 9px; transition: transform .15s ease; }
.zeitwahl[open] .zeitwahl__pfeil { transform: rotate(180deg); }


/* z-index 5: above the topbar's own children and above the sticky matrix
   columns (1) — the panel is a transient overlay, nothing may cut into it. */.zeitwahl__panel {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 5;
  display: flex; align-items: stretch;
  background: var(--surface);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  box-shadow: 0 16px 40px rgb(5 33 63 / .16);
  overflow: hidden;
}
/* The quick picks are the shortcut, the month grid is the choice — so they
   read as a list of links, not as eight framed buttons competing with it.
   The active pick is marked by weight and a rule as well as colour. */
.zeitwahl__schnell {
  padding: var(--sp-2);
  border-right: 1px solid var(--border-color);
  min-width: 168px;
  background: var(--bg);
}
.zeitwahl__schnell .nav-link {
  --bs-nav-link-color: var(--fg-2);
  --bs-nav-link-hover-color: var(--color-heading-darken);
  padding: 7px 10px 7px 12px;
  font-size: 13px;
  font-weight: var(--fw-medium);
  border-left: 2px solid transparent;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.zeitwahl__schnell .nav-link:hover { background: var(--bg-alt); }
.zeitwahl__schnell .nav-link.active {
  color: var(--color-heading-darken);
  background: var(--color-primary-light);
  border-left-color: var(--accent);
  font-weight: var(--fw-semibold);
}

/* The right side owns the panel's height: the grid spreads over whatever the
   quick-pick column is tall, and the footer sits at the bottom. Otherwise the
   months cram into the top and leave a hole underneath. */
.zeitwahl__frei {
  position: relative; padding: var(--sp-4);
  display: flex; flex-direction: column; flex: 1;
}
.zeitwahl__frei > footer { margin-top: auto; }
.zeitwahl__jahrnav { position: absolute; top: var(--sp-4); right: var(--sp-4); display: flex; gap: 4px; }
.zeitwahl__nav {
  width: 26px; height: 26px;
  border: 1px solid var(--border-color); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--fg-2);
  font-size: 14px; line-height: 1;
  cursor: pointer;
}
.zeitwahl__nav:hover:not(:disabled) { background: var(--bg-alt); color: var(--fg-1); }
.zeitwahl__nav:disabled { opacity: .35; cursor: default; }

.zeitwahl__jahre { display: flex; gap: var(--sp-6); flex: 1; }
.zeitwahl__jahr { display: flex; flex-direction: column; flex: 1; }
.zeitwahl__jahrtitel {
  margin: 0 0 var(--sp-3);
  font: var(--fw-semibold) 14px var(--font-display);
  color: var(--fg-1);
  text-align: center;
}
.zeitwahl__gitter {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 3px;
  flex: 1; align-content: stretch;
}
.zeitwahl__monat {
  min-width: 52px; padding: 8px 6px;
  border: 0; border-radius: var(--radius-sm);
  background: none;
  font: var(--fw-regular) 13px var(--font-body);
  color: var(--fg-1);
  cursor: pointer;
}
.zeitwahl__monat:hover:not(:disabled) { background: var(--bg-alt); }
.zeitwahl__monat:disabled { color: var(--fg-3); opacity: .4; cursor: default; }

/* Range endpoints filled, the months between them tinted — the same reading
   as the main picker's day ranges. */.zeitwahl__monat--drin { background: var(--color-primary-light); border-radius: 0; }
.zeitwahl__monat--rand { background: var(--color-primary); color: #fff; font-weight: var(--fw-medium); }
.zeitwahl__monat--rand:hover:not(:disabled) { background: var(--color-primary-darken); }

.zeitwahl__noscript { display: flex; gap: var(--sp-4); margin-top: var(--sp-3); }

.zeitwahl__fuss {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  margin-top: var(--sp-4); padding-top: var(--sp-3);
  border-top: 1px solid var(--border-color);
}
.zeitwahl__anzeige { font: var(--fw-medium) 13px var(--font-body); color: var(--fg-2); white-space: nowrap; }
.zeitwahl__aktionen { display: flex; gap: var(--sp-2); }
.zeitwahl__knopf2 {
  padding: 8px 14px;
  border: 1px solid var(--border-color); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--fg-2);
  font: var(--fw-medium) 13px var(--font-display);
  cursor: pointer;
}
.zeitwahl__knopf2:hover { background: var(--bg-alt); color: var(--fg-1); }
.zeitwahl__knopf2--primaer { border-color: var(--color-primary); background: var(--color-primary); color: #fff; }
.zeitwahl__knopf2--primaer:hover { background: var(--color-primary-darken); color: #fff; }


/* Small screens: the panel would overflow sideways — pin it under the topbar
   across the viewport and let the year panels wrap. */@media (max-width: 760px) {
  .zeitwahl__panel {
    position: fixed; left: var(--sp-3); right: var(--sp-3); top: calc(var(--topbar-h) + 6px);
    max-height: calc(100vh - var(--topbar-h) - 24px);
    overflow: auto;
    flex-direction: column;
  }
  .zeitwahl__liste { flex-direction: row; flex-wrap: wrap; border-right: 0; border-bottom: 1px solid var(--border-color); }
  .zeitwahl__jahre { flex-wrap: wrap; }
}


/* ---------- Trade detail page (stage 5) ---------- */
/* Links inside tables and chips inherit the text colour and reveal
   themselves on hover — a blue column would shout over the figures. */.tabellenlink { color: inherit; text-decoration: none; }
.tabellenlink:hover { text-decoration: underline; }


/* The ticket title on its own line, wrapping instead of clipping —
   nothing on this page cuts a title off. max-width keeps a marathon
   title from stretching the column; it wraps at ~46ch instead. */.ticket-titel {
  display: block;
  max-width: 46ch;
  white-space: normal;
  overflow-wrap: anywhere;
  color: var(--fg-2);
  margin-top: 1px;
}


/* ---------- Grooming page: filter chips ---------- */


/* ---------- Filter bar: segmented groups + switch ---------- */
/* One-of-n filters render as joined segments so the mutual exclusion is
   visible in the shape itself; the lone boolean gets a switch. Loose pills
   made the three exclusive amount states look like independent toggles
   (Dave, 23.08.). */.filterleiste {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  padding-top: 10px;
}


/* Filter links land here after the reload — clear the fixed topbar. */#alterung { scroll-margin-top: calc(var(--topbar-h) + 12px); }


/* The id and its arrow are one token — a line break between them reads as
   two separate things. */.ticketlink { white-space: nowrap; }


/* The order title, which is what actually decides an ambiguous case.
   NOT .clip--titel: that carries `overflow-wrap: anywhere`, which drops the
   cell's min-content width to a single character — the table then hands the
   column almost nothing and "Wartung Lüftungsgeräte" arrives as four broken
   fragments. `break-word` keeps the longest word as the minimum, so the
   column claims the width it needs and only a genuine monster word breaks. */.pflege-titel {
  min-width: 16ch;
  max-width: 40ch;
  white-space: normal;
  overflow-wrap: break-word;
}


/* ---------- Pipeline page ---------- */
/* The weighted-pipeline swatch: hatched like its bars in the chart, because
   contracted floor and weighted maybe must not look like the same substance
   in the legend either. */.legende__farbe--schraffur {
  background: repeating-linear-gradient(45deg,
    rgba(21, 157, 169, .55) 0 1.5px, rgba(21, 157, 169, .15) 1.5px 5px);
  border: 1px solid #159da9;
}


/* ---------- Assumptions form (pipeline) ---------- */
/* Structured fields instead of the one running line the form began as:
   every dial gets its label above, its unit inside, and its measured basis
   underneath — the reader adjusts a figure while seeing where it came from. */


.feld-hilfe { font-size: var(--fs-xs); color: var(--fg-3); }



/* ---------- Drill-down: a figure that opens its bookings ----------
   Looks like the number it is, says it is clickable on hover and focus.
   A button, so keyboard and screen readers reach it. */
.posten-link {
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  font: inherit;
  text-align: inherit;
  cursor: pointer;
  text-decoration: underline dotted transparent;
  text-underline-offset: 3px;
  transition: text-decoration-color .15s var(--ease-standard), color .15s var(--ease-standard);
}
.posten-link:hover,
.posten-link:focus-visible {
  color: var(--bs-link-color);
  text-decoration-color: currentColor;
}
.posten-panel { --bs-offcanvas-width: min(720px, 100vw); }
