Ctrl K
ComunidadGuías…

styles.css

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/* ═══════════════════════════════════════════════════════════════
   Meistertracker — UI Redesign
   ═══════════════════════════════════════════════════════════════ */

/* -- CSS Variables -- */
:root {
  /* One step down the same green, so a white label on it can be read.
     #16a34a carries white at 3.30:1 — under the 4.5 normal text needs, on every
     primary button, .stab.active and scan-tab count, and under 4.5 again at the
     sixteen places this token is the text colour rather than the fill. The lab
     reads this screen in a grow room, gloved, at arm's length, and 3.30 is the
     kind of number you feel as squinting rather than see as a defect.

     Not a new colour: #15803d was already --c-primary-hover, already on these
     exact buttons, already past somebody's eye. Moving it from the hover state
     to the resting state is the smallest change that clears AA — and it fixes
     the odder half of the old arrangement, where the label only became legible
     after you had committed to tapping it.

     The hover step moves down with it to #166534, which is --c-green-dark's
     value, so pressing still reads as a change. Nothing in the app puts dark
     text on this colour — checked, all twenty-one background uses pair it with
     white or carry no text — so there is nothing for the darkening to cost. */
  --c-primary: #15803d;
  --c-primary-light: #f0fdf4;
  --c-primary-hover: #166534;
  --c-bg: #f8fafc;
  --c-surface: #ffffff;
  --c-border: #e2e8f0;
  --c-border-light: #f1f5f9;
  --c-text: #1e293b;
  --c-text-sec: #475569;
  --c-text-muted: #64748b;
  /* Two names this sheet has always used and never defined.

     --c-accent arrived with the dashboard summary strip and spread to fifteen
     places without ever getting a value; --c-card to two. An undefined custom
     property is not "falls back to something sensible" — the declaration is
     invalid at computed-value time, so an inherited property takes the parent's
     value and everything else takes its initial one. That is why the substrate
     progress bar had a transparent fill, why "open" — the commonest substrate
     status — drew its pill in whatever colour it inherited, and why today's
     date in the calendar looked like every other day.

     They are aliases, not new colours: every site that reads them wants the
     surface or the brand green this sheet already has a name for. Defining them
     as aliases keeps them honest — change --c-primary and the accent follows. */
  --c-accent: var(--c-primary);
  --c-card: var(--c-surface);
  /* The step this palette was missing.

     Each family has light / border / accent / mid / dark. None of those is a
     surface you can put white text on: the mid tones read 3.8:1 (red) and
     2.2:1 (amber) against white, and the -dark tones are so heavy they read as
     disabled on a small control. So every author who needed a filled button
     picked a one-off — #dc2626 six times, #ea580c eight. That is not
     carelessness, it is the palette being short a step, and the two guesses
     did not land in the same place: #dc2626 clears 4.5:1, #ea580c does not.

     So the step gets a name and one value per family, chosen from what the app
     already contained: #dc2626 is kept as-is (4.83:1) and #b45309, which was
     already here once, replaces #ea580c (3.56 -> 5.02). "Strong" means exactly
     one thing — the tone that carries white text at 4.5:1. */
  --c-red-strong: #dc2626;
  --c-amber-strong: #b45309;
  /* ── Production stage colours ─────────────────────────────────────────
     The Chargen tiles paint their bar and their three stage figures with
     these. They were referenced from app.js before they existed anywhere:
     `background: var(--st-inc)` with no fallback is invalid at computed-value
     time and background-color does not inherit, so every segment of the
     proportion bar rendered transparent and the bar — the point of the tile —
     was a flat grey pill on every Sorte.

     Values are the muted zone palette (ZONE_SEED_COLOR in db.js), so a phase
     looks the same whether it is a bar segment here, a badge in the Status
     column or a dot on a rack. Grain is the slate end: it is the raw material,
     not yet a mushroom, and it reads as the "not started" step. */
  --st-spawn: #6b7a8c;
  --st-inc: #4d829b;
  --st-fruit: #438871;
  /* Semantic color palette */
  --c-red: #ef4444;
  --c-red-light: #fee2e2;
  --c-red-dark: #991b1b;
  --c-red-border: #fecaca;
  --c-red-accent: #fca5a5;
  --c-blue: #3b82f6;
  --c-blue-light: #dbeafe;
  --c-blue-dark: #1e40af;
  --c-blue-border: #bfdbfe;
  --c-blue-accent: #93c5fd;
  --c-green: #22c55e;
  --c-green-light: #dcfce7;
  --c-green-dark: #166534;
  --c-green-border: #bbf7d0;
  --c-green-accent: #86efac;
  --c-amber: #f59e0b;
  --c-amber-light: #fef3c7;
  --c-amber-dark: #92400e;
  --c-amber-border: #fde68a;
  --c-amber-accent: #fcd34d;
  --c-purple: #8b5cf6;
  --c-purple-light: #f3e8ff;
  --c-purple-dark: #6b21a8;
  --c-purple-border: #e9d5ff;
  --c-purple-accent: #c084fc;
  --c-slate: #64748b;
  --c-teal: #14b8a6;
  --c-teal-light: #ccfbf1;
  --c-teal-dark: #115e59;
  --c-teal-border: #99f6e4;
  --c-indigo: #6366f1;
  --c-indigo-light: #e0e7ff;
  --c-indigo-dark: #3730a3;
  --c-indigo-border: #c7d2fe;
  --sidebar-w: 240px;
  --sidebar-collapsed: 64px;
  --radius: 12px;
  --radius-sm: 8px;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);

  /* ── SIZE / SPACE / TYPE ─────────────────────────────────────────────────
     These values are the PHONE. The desktop is the override below.

     It used to run the other way: the base was the desktop and each phone rule
     subtracted from it, which is how a 480 px viewport ended up with a smaller
     primary button than a 27" monitor. Nobody decided that; it is just what you
     get when "mobile" means "the same thing, tighter". The lab works standing,
     gloved, one-handed, so the phone gets the generous end of every pair and
     the desk has to ask for the tight one.

     Rule for adding to this block: a token's :root value is what a phone should
     get. If that is also what the desktop should get, it does not need a line
     in the override. */
  --fs-xs: 0.8125rem;
  --fs-sm: 0.9375rem;
  --fs-base: 1.0625rem;
  /* 12px is the single most common inline size in this app — filter selects,
     status lines, card blurbs, preview boxes. --fs-xs cannot absorb them: it is
     11px on a desktop and would drop every one of them by a pixel. So it gets
     its own pair, phone at the floor and desktop where it already was. */
  --fs-meta: 0.8125rem;
  /* And the same for 10px, the size id chips and timestamps were written at.
     Three pairs cover 497 of the 497 inline sizes in this app; the six
     fractional ones in between are named where they are used. */
  --fs-micro: 0.8125rem;
  /* The floor, as a sentinel rather than a size. 104 rules in this stylesheet
     set a font-size below --fs-xs and serve both devices from that one number:
     `.sec` at 12px, `.sb-group-label` at 10px, `.chip-l` at 9px. Every one of
     those numbers is a considered DESKTOP value, so none of them may move.

     `font-size: max(12px, var(--fs-min))` says both things at once. On a phone
     the floor wins and the text is 13px; on a desk --fs-min is 0 and max() gives
     back exactly the 12px that was written, byte for byte. The desktop cannot
     move here — not "was checked and did not", cannot — because the value is
     never replaced, only floored on the device that needs a floor.

     The alternative was five paired tokens, one per distinct desktop value
     (9, 10, 11.5, 12, 12.5). Rejected: five hand-written pairs is five chances
     to type the wrong desktop number, and test/desktop-baseline.json watches 31
     selectors out of the 104 rules — it is a tripwire, not a proof, and this is
     exactly the shape of change it cannot see.

     Same trick as --tap: auto below, and for the same reason — the desktop's
     honest answer is "no constraint", not "a different constraint". */
  --fs-min: 0.8125rem;
  /* 44px is WCAG 2.5.5 AAA. 56 is the deliberate step past it for gloves — and
     it is not a new number here: .bottom-nav-btn already chose 56px the one
     time this codebase sat down and thought about a touch target. */
  --tap: 56px;
  --tap-sm: 48px;
  /* The touch floor, as a sentinel — the --fs-min idea applied to height.
     Rules all over the app already chose a minimum and chose it thoughtfully:
     44px on the flow's footer buttons, 46px on its batch rows, 48px on its zone
     buttons. Those are WCAG-correct numbers for a mouse and a considered
     desktop layout, and replacing them would move three desktop values to buy
     nothing.

       min-height: max(46px, var(--tap-min));

     reads as "46, and at least 56 in a hand". --tap itself cannot do this job:
     its desktop value is `auto`, which is not a length, and max() with a
     keyword is invalid. So this is the numeric twin — 0 on a desktop, which
     computes identically to `auto` for min-height (the fixture records 0px for
     every --tap consumer). */
  --tap-min: 56px;
  /* And the same sentinel for the other floor, because §9 chose two. --tap-min
     is 56px for a gloved hand on a Feld screen; this is the 48px one, and the
     rule for picking is which kind of screen the control lives on. Chrome that
     rides along on every screen — the topbar bell, the drawer toggle, the undo
     toast — takes this one: it clears the 44px minimum every guideline agrees
     on without spending 56px of a phone's height on furniture. Having only the
     Feld floor as a sentinel is what left that chrome at 26–36px through four
     phases; there was nothing to floor it with that did not also make it huge. */
  --tap-sm-min: 48px;
  --pad-page: 16px;
  --pad-card: 16px;
  --pad-btn: 13px 18px;
  --pad-stab: 11px 18px;
  --pad-sb: 12px 14px;
  --pad-modal: 16px 14px;
  /* Where the floating chrome sits above the bottom edge. On a phone the 56px
     bottom nav is in the way; on a desk there is no bar — same inversion as
     everything above. Three pieces of floating chrome used to carry three
     unrelated numbers here — a 72px shelf, the 24px .cam-fab was born with, and
     an .undo-bar offset derived by hand from the second. One token now, and the
     derivation stays visible. */
  --fab-bottom: 72px;
  /* The launcher's tiles. A pair rather than a max() floor, because this is not
     a floor: 14.5px already clears --fs-min, and 17px on a phone is a decision
     about what the first screen a worker meets should look like, not a rescue.
     max() is for "never smaller than"; a pair is for "this size here, that size
     there", and keeping the two apart is what stops either from becoming the
     answer to everything. The padding grows with the type — the title gains
     2.5px inside a box that would otherwise stay the same size. */
  --fs-tile: 1.0625rem;
  --pad-tile: 18px 18px 20px;
}

/* The desktop override restores today's values exactly — every number below was
   read off the computed styles before the conversion, and test/desktop-baseline.json
   plus scripts/capture-desktop-baseline.js exist to prove it stayed that way.

   `hover: hover` is doing real work in this query, not decoration. The reason
   for a big touch target is the input device, not the viewport width: a tablet
   held in a gloved hand is 1024 px wide and still needs the phone's numbers.
   Coarse-pointer devices therefore keep the :root values at any width, which is
   also what the old `@media (pointer: coarse)` .sb-btn block was reaching for
   before the tokens absorbed it. A touchscreen laptop reports hover: hover for
   its mouse and lands on the desktop side, correctly.

   `auto` for the tap tokens is not a placeholder — it means "intrinsic height,
   exactly as before", so min-height contributes nothing on a desktop. */
/* …and print, which is neither a phone nor a desk but is much closer to the
   desk. A printed page has no pointer at all, so `hover: hover` is false for
   it and every token above resolved to the value chosen for a gloved hand:
   13px floors on a barcode label, and `min-height: max(46px, var(--tap-min))`
   putting 56px of white space under every control that reached paper. Nothing
   read a token from inside @media print, so this was latent rather than
   broken — until the inline sizes moved onto classes that do. */
@media (min-width: 769px) and (hover: hover), print {
  :root {
    --fs-xs: 0.6875rem;
    --fs-sm: 0.8125rem;
    --fs-base: 0.9375rem;
    --fs-meta: 0.75rem;
    --fs-micro: 0.625rem;
    /* 0, so every max() in the sheet returns the literal beside it unchanged. */
    --fs-min: 0px;
    /* ⚠️ **24 und nicht `auto`** (Paket P1, Regel R1). `auto` hieß „keine
       Untergrenze am Rechner", und das war die fehlende Hälfte der Achse: Die
       grobe Seite ist hier seit den Marken richtig, weil sie an `hover: hover`
       hängt und nicht an einer Breite, aber die feine Seite hatte gar keine
       eigene Marke. 24 px sind WCAG 2.5.8 „Target Size (Minimum)", Stufe AA,
       und die Stufe kennt kein Eingabegerät: Sortierknöpfe der Chargentabelle,
       „Bearb." und „Drucken" in Sorten und Labor und „Wochenrhythmus
       bearbeiten" mit 13 px verletzen sie mit der Maus genauso wie mit dem
       Finger. Die 56 und 48 der Handseite bleiben davon unberührt; wo im Blatt
       `max(44px, …)` steht, gewinnt weiter die 44. */
    --tap: 24px;
    --tap-sm: 24px;
    --tap-min: 24px;
    --tap-sm-min: 24px;
    --pad-page: 24px;
    --pad-card: 20px;
    --pad-btn: 9px 16px;
    --pad-stab: 7px 16px;
    --pad-sb: 9px 12px;
    --pad-modal: 28px;
    /* No bottom nav here, so the chrome sits where it always did. Both FABs are
       display:none on a desktop anyway; only .undo-bar reads this off-phone,
       and its own rule keeps it out of the way. */
    --fs-tile: 0.90625rem;
    --pad-tile: 15px 15px 16px;
    --fab-bottom: 24px;
  }
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
body {
  font-family:
    system-ui,
    -apple-system,
    'Segoe UI',
    sans-serif;
  background: var(--c-bg);
  color: var(--c-text);
  font-size: var(--fs-base);
  line-height: 1.5;
  /* body used to carry `overflow-x: hidden`, and its absence is deliberate.
     It suppressed the sideways scrollbar, which is the one signal a person
     would have noticed, and that made "no horizontal scroll" a claim nobody
     could disprove: two mobile rounds asserted it and neither measured it.
     Re-measured on 2026-08-22 with scripts/measure-mobile.js --app, both ways,
     with the ratchet emptied so raw findings show: 46 touch, 4 overflow and
     2 zoomed-out either way, and 14 after-a-jump without the rule against 13
     with it. So it hid exactly ONE finding, and a jump one, not an overflow.
     (The first version of this comment said two overflow findings, 96 against
     98. That was counted while the outermost-offender filter was broken and
     every nested child counted separately.)
     What has not changed is the second half: the rule did not do the job it
     was there for. The pages that widen their layout viewport on a phone widen
     it with the rule in place, because that is the meta viewport and not a
     scrollbar. test/breakpoint-coverage.test.js keeps it gone. */
}

/* ═══ SIDEBAR ═══════════════════════════════════════════════════ */
.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--sidebar-w);
  background: var(--c-surface);
  border-right: 1px solid var(--c-border);
  display: flex;
  flex-direction: column;
  z-index: 120;
  transition:
    width 0.2s ease,
    transform 0.2s ease;
  overflow: hidden;
}
.sb-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 16px 12px;
  border-bottom: 1px solid var(--c-border-light);
  min-height: 56px;
}
.sb-logo {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--c-primary);
  flex-shrink: 0;
}
.sb-logo svg {
  flex-shrink: 0;
}
.sb-logo-text {
  font-size: 0.9375rem;
  font-weight: 700;
  color: var(--c-text);
  white-space: nowrap;
}
.sb-logo-text small {
  font-weight: 400;
  color: var(--c-text-sec);
  font-size: max(0.75rem, var(--fs-min));
}
.sb-toggle {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--c-text-sec);
  padding: 4px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s;
  flex-shrink: 0;
  /* 18px of icon in 4px of padding is a 26px target. The floor is a phone-only
     0-on-desktop sentinel, so the header keeps its desktop geometry exactly. */
  min-width: var(--tap-sm-min);
  min-height: var(--tap-sm-min);
}
.sb-toggle:hover {
  background: var(--c-border-light);
  color: var(--c-text);
}

.sb-nav,
.sb-admin-nav {
  flex: 1;
  overflow-y: auto;
  padding: 12px 10px;
}
.sb-group-label {
  font-size: max(0.625rem, var(--fs-min));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-text-muted);
  padding: 12px 10px 6px;
}
.sb-group-label:first-child {
  padding-top: 4px;
}
.sb-btn {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: var(--pad-sb);
  min-height: var(--tap-sm);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--c-text-sec);
  font-size: var(--fs-sm);
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
  position: relative;
}
.sb-btn svg {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
}
/* Touch-device sidebar items: bump to 48 px min-height + 15 px font so the
   drawer (opened via the hamburger on mobile) has finger-friendly targets.
   Defaults below stay at 38 px for desktop where a mouse cursor is precise. */
/* The size half of this block is gone — --tap-sm, --fs-sm and --pad-sb now say
   the same thing for every coarse pointer, at any width. The icon is not a
   token, so it stays. */
@media (pointer: coarse) {
  .sb-btn svg {
    width: 20px;
    height: 20px;
  }
}
.sb-btn:hover {
  background: var(--c-border-light);
  color: var(--c-text);
}
.sb-btn.active {
  background: var(--c-primary-light);
  color: var(--c-primary);
}
.sb-btn.active svg {
  stroke: var(--c-primary);
}
.sb-btn.alert::after {
  content: '';
  position: absolute;
  top: 8px;
  right: 8px;
  width: 7px;
  height: 7px;
  background: #ef4444;
  border-radius: 50%;
}

.sb-footer {
  border-top: 1px solid var(--c-border-light);
  padding: 10px;
}
/* Admin navigation — on a desktop the Admin section borrows the sidebar.
   Fourteen sub-tabs do not fit one horizontal strip: .stabs has neither wrap
   nor scroll outside the mobile rules, so the pills squeeze until their labels
   break. A vertical, grouped list has no such ceiling, and .sb-nav already
   scrolls.

   Desktop only, and the min-width is the whole point. The drawer used to swap
   the same way and stay open on a phone, because closing it left the section
   with nothing to navigate by. The strip is now a drill-down list on the page
   itself, so a swapped drawer would lay the same thirteen entries over the
   thirteen behind it — and take the screen with them. Below 769px the drawer
   keeps the main navigation, which is what a phone opens it for. */
.sb-admin-nav {
  display: none;
}
@media (min-width: 769px) {
  body.admin-mode .sb-nav {
    display: none;
  }
  body.admin-mode .sb-admin-nav {
    display: block;
  }
}
/* The footer button is the door, and one door needs one place: the same spot
   leads in and back out again. Solid green while inside says which side of it
   you are standing on, and the arrow replacing the gear says what pressing it
   does — a gear that navigates away would be telling you the wrong thing. */
#n-settings .sb-ico-back {
  display: none;
}
body.admin-mode #n-settings {
  background: var(--c-primary);
  color: #fff;
}
body.admin-mode #n-settings svg {
  stroke: #fff;
}
body.admin-mode #n-settings .sb-ico-admin {
  display: none;
}
body.admin-mode #n-settings .sb-ico-back {
  display: block;
}
@media (min-width: 769px) {
  body.admin-mode #p-settings > .stabs {
    display: none;
  }
}

.sb-sync {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
}
/* One line, always. The text under it changes every five seconds — "gerade
   eben", then "vor 12 Min." — and wrapping to a second line made the row taller
   and pushed the Admin button above it up by a line. A navigation button that
   moves while nobody touches it is one the hand has to re-aim at, and it moved
   on a timer. min-width: 0 is what lets a flex child shrink far enough to
   ellipsize instead of forcing the row wider. */
.sb-sync-label {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sb-lang-sel {
  width: auto;
  font-size: max(0.6875rem, var(--fs-min));
  padding: 3px 6px;
  border: 1px solid var(--c-border);
  border-radius: 6px;
  background: var(--c-surface);
  color: var(--c-text-sec);
  cursor: pointer;
  font-family: inherit;
}

/* Sidebar collapsed (desktop) */
.sidebar.sb-collapsed {
  width: var(--sidebar-collapsed);
}
.sidebar.sb-collapsed .sb-logo-text,
.sidebar.sb-collapsed .sb-btn span,
.sidebar.sb-collapsed .sb-group-label,
.sidebar.sb-collapsed .sb-sync-label,
.sidebar.sb-collapsed .sb-lang-sel {
  display: none;
}
.sidebar.sb-collapsed .sb-header {
  justify-content: center;
  padding: 16px 8px 12px;
}
.sidebar.sb-collapsed .sb-btn {
  justify-content: center;
  padding: 9px 0;
}
.sidebar.sb-collapsed .sb-sync {
  justify-content: center;
  padding: 8px 4px;
}
.sb-is-collapsed .content-wrap {
  margin-left: var(--sidebar-collapsed);
}

/* Worktree warning banner — fixed at the very top of the viewport so it
   floats over the sidebar + topbar; unmissable amber on dark text. Body
   gets .has-worktree-banner when active to nudge layout down.
   The [hidden] override below has higher specificity (0,2,0) than the
   base .worktree-banner rule (0,1,0) — otherwise `display: flex` would
   defeat the UA `[hidden] { display: none }` rule and the banner would
   ship visible to every prod user. */
.worktree-banner[hidden] {
  display: none;
}
.worktree-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: repeating-linear-gradient(45deg, #f59e0b 0 14px, #fbbf24 14px 28px);
  color: #1f1300;
  font-weight: 600;
  font-size: 0.8125rem;
  padding: 8px 16px;
  padding-top: calc(8px + env(safe-area-inset-top, 0px));
  padding-left: calc(16px + env(safe-area-inset-left, 0px));
  padding-right: calc(16px + env(safe-area-inset-right, 0px));
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  border-bottom: 2px solid #b45309;
  text-align: center;
  line-height: 1.35;
}
.worktree-banner-icon {
  font-size: 1.125rem;
  line-height: 1;
}
.worktree-banner-text b {
  font-weight: 800;
  letter-spacing: 0.3px;
}
/* Push fixed-positioned chrome (sidebar, mobile topbar) down so the banner
   doesn't visually cover them. content-wrap flows naturally and is shifted
   via body padding-top. The banner text wraps on narrow viewports, so the
   inline probe sets --worktree-banner-h to the actual height via
   ResizeObserver; 38px is the single-line fallback for the first paint
   before the script runs. */
body.has-worktree-banner {
  padding-top: var(--worktree-banner-h, 38px);
}
body.has-worktree-banner .sidebar {
  top: var(--worktree-banner-h, 38px);
}
body.has-worktree-banner .mobile-topbar {
  top: var(--worktree-banner-h, 38px);
}

/* Mobile topbar */
.mobile-topbar {
  display: none;
  position: sticky;
  top: 0;
  z-index: 110;
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border);
  padding: 10px 16px;
  padding-top: calc(10px + env(safe-area-inset-top, 0px));
  padding-left: calc(16px + env(safe-area-inset-left, 0px));
  padding-right: calc(16px + env(safe-area-inset-right, 0px));
  align-items: center;
  gap: 12px;
}
.sb-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  z-index: 115;
  opacity: 0;
  transition: opacity 0.2s;
}
.sb-overlay.sb-show {
  display: block;
  opacity: 1;
}
/* The drawer is a phone arrangement and so is its overlay, but this class knew
   nothing about width: a drawer opened at 760px and a window then widened left
   the sheet lying over the desktop, where nothing offered to take it off. app.js
   now clears the class on the crossing, and this is the second lock on the same
   door. It is worth having twice, because the service worker can be serving a
   cached app.js from before that fix while this stylesheet is already new. */
@media (min-width: 769px) {
  .sb-overlay.sb-show {
    display: none;
  }
}
/* The scan button sits at z-index 850, far above the sidebar's 120, so with the
   drawer open it floats over the drawer's footer and buries the Admin entry and
   the language picker. The drawer is a modal surface; the button does not act on
   it, and the overlay behind it is already swallowing taps. */
body.sb-mobile-open .cam-fab {
  display: none;
}
/* Same argument, one layer up: a dialog's backdrop is z-index 200 against the
   button's 850, so it floats over all 25 of them — and not merely visibly.
   elementFromPoint at its centre returns the button while a dialog is open, so
   a thumb aiming at the bottom of a form opens the scanner instead. Found while
   measuring the guided flow at 375px, where it overlapped the modal's corner.

   :has() rather than a body class from JS, because every dialog in this app
   opens the same way — .modal-bg gains .open — so one selector covers all of
   them and the next one too. The pattern is already in the sheet
   (.notif-fab:has(.bell-badge:not([hidden]))). */
body:has(.modal-bg.open) .cam-fab {
  display: none;
}

/* ═══ CONTENT WRAP ══════════════════════════════════════════════ */
.content-wrap {
  margin-left: var(--sidebar-w);
  min-height: 100vh;
  min-height: 100dvh;
  transition: margin-left 0.2s ease;
}

/* ═══ SCAN MODAL (centered overlay) ════════════════════════════ */
.scan-overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  background: rgba(34, 120, 60, 0.35);
  display: none;
  align-items: center;
  justify-content: center;
  transition: background 0.3s ease;
}
.scan-overlay.open {
  display: flex;
}
.scan-overlay.scan-bg-ok {
  background: rgba(34, 120, 60, 0.35);
}
.scan-overlay.scan-bg-err {
  background: rgba(180, 30, 30, 0.4);
}
.scan-modal {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: 12px;
  box-shadow: 0 12px 48px rgba(0, 0, 0, 0.22);
  padding: 0;
  width: 80vw;
  height: 80vh;
  max-width: 1000px;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden;
  transition: box-shadow 0.25s ease;
}
.scan-overlay.scan-bg-ok .scan-modal {
  box-shadow:
    0 12px 48px rgba(0, 0, 0, 0.22),
    0 0 0 3px rgba(34, 197, 94, 0.55);
}
.scan-overlay.scan-bg-err .scan-modal {
  box-shadow:
    0 12px 48px rgba(0, 0, 0, 0.22),
    0 0 0 3px rgba(239, 68, 68, 0.6);
}
.scan-modal-close {
  position: absolute;
  top: 4px;
  right: 4px;
  background: none;
  border: none;
  font-size: 1.75rem;
  cursor: pointer;
  color: var(--c-text-muted);
  line-height: 1;
  width: max(44px, var(--tap-min));
  height: max(44px, var(--tap-min));
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  transition: background 0.15s;
  z-index: 2;
}
.scan-modal-close:hover {
  background: rgba(0, 0, 0, 0.06);
  color: var(--c-text);
}
/* Icon close button used in modal headers (bag-info, etc). 44px touch target,
   inherits modal text colour, neutral hover. Used in place of the text-pill
   close that workers struggled to hit one-handed on a phone. */
.modal-close-icon {
  width: max(44px, var(--tap-min));
  height: max(44px, var(--tap-min));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--c-text-muted);
  padding: 0;
  margin: -8px -8px -8px 0;
  transition: background 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.modal-close-icon:hover {
  background: rgba(0, 0, 0, 0.06);
  color: var(--c-text);
}
.modal-close-icon:active {
  background: rgba(0, 0, 0, 0.12);
}
.scan-modal-header {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 1rem;
  font-weight: 700;
  color: var(--c-text);
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--c-border);
  transition:
    background 0.2s,
    border-color 0.2s;
}
/* Action-colored header bands */
.scan-modal.scan-action-add .scan-modal-header {
  background: #f0fdf4;
  border-bottom-color: #86efac;
}
.scan-modal.scan-action-move .scan-modal-header {
  background: #eff6ff;
  border-bottom-color: #93c5fd;
}
.scan-modal.scan-action-move_batch .scan-modal-header {
  background: #eef2ff;
  border-bottom-color: #a5b4fc;
}
.scan-modal.scan-action-remove .scan-modal-header {
  background: #fef2f2;
  border-bottom-color: #fca5a5;
}
.scan-modal.scan-action-harvest .scan-modal-header {
  background: #fffbeb;
  border-bottom-color: #fcd34d;
}

.scan-modal-tabs {
  display: flex;
  gap: 2px;
  padding: 0 12px;
  background: var(--c-bg);
  border-bottom: 1px solid var(--c-border);
  flex-shrink: 0;
}
.scan-tab {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 10px 16px;
  /* --fs-sm's desktop value is 13px, exactly what this had, and --tap resolves
     to auto there — so both of these are phone-only in effect. The fixture
     records fontSize 13px / minHeight 0px for .scan-tab and still does. */
  min-height: var(--tap);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--c-text-muted);
  cursor: pointer;
  font-family: inherit;
  transition:
    color 0.15s,
    border-color 0.15s,
    background 0.15s;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.scan-tab:hover {
  color: var(--c-text);
  background: rgba(0, 0, 0, 0.03);
}
.scan-tab.active {
  color: var(--c-primary);
  border-bottom-color: var(--c-primary);
}
.scan-tab-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--c-border);
  color: var(--c-text-muted);
  font-size: max(0.625rem, var(--fs-min));
  font-weight: 700;
}
.scan-tab.active .scan-tab-count {
  background: var(--c-primary);
  color: #fff;
}
.scan-modal-body {
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  flex: 1;
  overflow: hidden;
  min-height: 0;
}
.scan-tab-panel {
  display: none;
  flex: 1;
  flex-direction: column;
  gap: 12px;
  min-height: 0;
  overflow: hidden;
}
.scan-tab-panel.active {
  display: flex;
}
.scan-successes-list {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-right: 4px;
}
.scan-successes-empty {
  padding: 40px 20px;
  text-align: center;
  color: var(--c-text-muted);
  font-size: 0.8125rem;
  font-style: italic;
}
.scan-successes-list:not(:empty) + .scan-successes-empty,
.scan-successes-list:not(:empty) ~ .scan-successes-empty {
  display: none;
}
.scan-success-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: var(--c-bg);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  font-size: max(0.75rem, var(--fs-min));
}
.scan-success-time {
  color: var(--c-text-muted);
  font-family: ui-monospace, 'Cascadia Code', monospace;
  font-size: max(0.6875rem, var(--fs-min));
  flex-shrink: 0;
}
.scan-success-body {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.scan-success-body b {
  font-family: ui-monospace, 'Cascadia Code', monospace;
}
.scan-success-loc {
  color: var(--c-text-muted);
  font-size: max(0.6875rem, var(--fs-min));
}
.scan-success-undo {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  /* 4px of vertical padding and nothing else: the target was as tall as its
     own text. This is the button that takes back a wrong scan, and it appears
     on a flash that is already going away. */
  min-height: var(--tap);
  min-width: var(--tap);
  padding: 4px 10px;
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
  flex-shrink: 0;
  transition: all 0.15s;
}
.scan-success-undo:hover {
  background: #fee2e2;
  color: #991b1b;
  border-color: #fca5a5;
}

.scan-modal-chips {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
  gap: 6px;
}
.chip {
  background: var(--c-bg);
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  text-align: center;
  min-width: 50px;
  transition:
    background 0.2s,
    box-shadow 0.2s;
}
.chip-l {
  font-size: max(0.5625rem, var(--fs-min));
  color: var(--c-text-muted);
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.chip-v {
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--c-text);
  margin-top: 2px;
}
.chip-last-scan {
  background: #f0f9ff;
  border: 1px solid #bfdbfe;
}
.chip-last-scan .chip-v {
  font-family: ui-monospace, 'Cascadia Code', monospace;
  font-size: max(0.6875rem, var(--fs-min));
}
/* Chip pulse animation for missing input hints */
.chip-pulse {
  animation: chip-pulse 1.5s ease-in-out infinite;
}
@keyframes chip-pulse {
  0%,
  100% {
    box-shadow: none;
  }
  50% {
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
  }
}

.scan-modal-btns {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
  padding: 12px 20px;
  border-top: 1px solid var(--c-border);
}
.btn-xs {
  padding: 6px 12px;
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 500;
  border-radius: var(--radius-sm);
  cursor: pointer;
  border: 1px solid var(--c-border);
  background: var(--c-surface);
  font-family: inherit;
  white-space: nowrap;
  transition: all 0.15s;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.btn-xs:hover {
  background: var(--c-bg);
}
.btn-xs.green {
  background: var(--c-primary-light);
  color: #166534;
  border-color: #bbf7d0;
}
.btn-xs.green:hover {
  background: #dcfce7;
}
.btn-end-session {
  background: #fef3c7;
  color: #92400e;
  border-color: #fcd34d;
}
.btn-end-session:hover {
  background: #fde68a;
}

/* -- Scan toast (inline in modal) -- */
.scan-toast-inline {
  padding: 10px 14px;
  border-radius: var(--radius);
  font-size: 0.9375rem;
  font-weight: 600;
  font-family: ui-monospace, 'Cascadia Code', monospace;
  text-align: center;
  word-break: break-word;
  display: none;
  transition: opacity 0.2s;
}
.scan-toast-inline.visible {
  display: block;
}
.scan-toast-inline.fb-ok {
  background: #dcfce7;
  color: #166534;
  border: 2px solid #86efac;
}
.scan-toast-inline.fb-err {
  background: #fee2e2;
  color: #991b1b;
  border: 2px solid #fca5a5;
}
.scan-toast-inline.fb-info {
  background: var(--c-surface);
  color: var(--c-text);
  border: 2px solid var(--c-border);
}
.scan-toast-inline.fb-warn {
  background: #fef9c3;
  color: #854d0e;
  border: 2px solid #fde047;
}
.scan-toast-inline.fb-harvest {
  background: #fef3c7;
  color: #92400e;
  border: 2px solid #fcd34d;
}

/* -- Scan log inside modal -- */
.scan-modal-log {
  flex: 1;
  min-height: 120px;
  overflow-y: auto;
  font-size: max(0.6875rem, var(--fs-min));
  font-family: ui-monospace, 'Cascadia Code', monospace;
  display: flex;
  flex-direction: column-reverse;
  gap: 3px;
}
.scan-modal-log:empty {
  display: none;
}
.scan-log-entry {
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--c-bg);
  color: var(--c-text-muted);
  display: flex;
  align-items: center;
  gap: 8px;
  position: relative;
}
.scan-log-entry.log-ok {
  border-left: 3px solid #86efac;
}
.scan-log-entry.log-err {
  border-left: 3px solid #fca5a5;
}
.scan-log-entry.log-info {
  border-left: 3px solid var(--c-border);
}
.scan-log-entry.log-harvest {
  border-left: 3px solid #fcd34d;
}
.scan-log-entry .sle-time {
  color: var(--c-text-muted);
  font-size: max(0.625rem, var(--fs-min));
  flex-shrink: 0;
}
.scan-log-entry .sle-msg {
  flex: 1;
}
.scan-log-entry .sle-undo {
  display: none;
  background: none;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  padding: 2px 6px;
  font-size: max(0.625rem, var(--fs-min));
  cursor: pointer;
  color: var(--c-text-muted);
  flex-shrink: 0;
  transition: all 0.15s;
}
.scan-log-entry:hover .sle-undo {
  display: inline-flex;
}
.scan-log-entry .sle-undo:hover {
  background: #fee2e2;
  color: #991b1b;
  border-color: #fca5a5;
}
/* Touch devices have no :hover — always show the undo button and bump it to a
   real tap target so workers can undo a mis-scan from the phone. It said that
   and then gave it 36px and 12px, both below the floors, on the one control
   that exists to correct a mistake made while scanning. */
@media (pointer: coarse) {
  .scan-log-entry .sle-undo {
    display: inline-flex;
    min-height: var(--tap);
    padding: 8px 12px;
    font-size: var(--fs-sm);
  }
}

/* -- Scan session summary -- */
.scan-session-summary {
  padding: 16px;
  border-radius: var(--radius);
  background: #f8fafc;
  border: 1px solid var(--c-border);
}
.scan-session-summary h3 {
  font-size: 0.875rem;
  font-weight: 700;
  margin: 0 0 12px;
}
.scan-summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}
.scan-summary-stat {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  text-align: center;
}
.scan-summary-stat .ss-num {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--c-text);
}
.scan-summary-stat .ss-lbl {
  font-size: max(0.625rem, var(--fs-min));
  color: var(--c-text-muted);
  text-transform: uppercase;
}
.scan-summary-batches {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
  margin-bottom: 12px;
}
.scan-summary-batches span {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-right: 8px;
}
.scan-summary-actions {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
}

/* -- Count bump animation -- */
@keyframes count-bump {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.35);
  }
  100% {
    transform: scale(1);
  }
}
.count-bump .chip-v {
  animation: count-bump 0.3s ease;
}

/* ═══ CAMERA SCAN MODAL ══════════════════════════════════════
   Wrapper inside #m-camscan that holds the cam-reader frame + HUD.
   Centred on desktop. On phones we anchor to the top so the camera
   sits in the upper third of the screen — workers can present a bag
   to the camera with one hand while the other hand reaches the
   bottom-area HUD buttons (audit Section 1.4 single-handed-reach
   recommendation). */
.cam-scan-wrap {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
@media (pointer: coarse) {
  .cam-scan-wrap {
    justify-content: flex-start;
    padding-top: 24px;
  }
}

/* ═══ CAMERA SCAN HUD (continuous scanning overlay) ══════════ */
.cam-hud {
  width: 100%;
  max-width: 500px;
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
}
.cam-hud-toast {
  width: 100%;
  text-align: center;
  font-size: 0.8125rem;
  font-weight: 600;
  padding: 0;
  max-height: 0;
  overflow: hidden;
  border-radius: 8px;
  transition:
    max-height 0.25s ease,
    padding 0.25s ease,
    opacity 0.25s ease;
  opacity: 0;
}
/* Platz für drei Zeilen. Eine Fehlermeldung sagt jetzt, was los ist UND was zu
   tun ist, und das passt nicht in die zwei Zeilen, die 60px hergaben — der Rest
   wurde abgeschnitten, und abgeschnitten war immer der Teil mit dem Ausweg. */
.cam-hud-toast.visible {
  max-height: 96px;
  padding: 8px 14px;
  opacity: 1;
}
.cam-hud-toast.ht-ok {
  background: #166534;
  color: #bbf7d0;
}
.cam-hud-toast.ht-err {
  background: #991b1b;
  color: #fecaca;
}
.cam-hud-toast.ht-warn {
  background: #854d0e;
  color: #fef08a;
}
.cam-hud-toast.ht-info {
  background: #1e3a5f;
  color: #bfdbfe;
}
.cam-hud-toast.ht-harvest {
  background: #78350f;
  color: #fde68a;
}

.cam-hud-chips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
}
.cam-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 600;
  padding: 4px 10px;
  border-radius: 99px;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  font-family: var(--ff-mono, monospace);
  transition:
    background 0.2s,
    transform 0.15s;
}
.cam-chip-l {
  font-size: max(0.625rem, var(--fs-min));
  font-weight: 400;
  opacity: 0.6;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.cam-chip-arrow {
  padding: 4px 2px;
  opacity: 0.4;
  font-size: 0.875rem;
}
.cam-chip.ch-set {
  background: rgba(255, 255, 255, 0.25);
}
.cam-chip.ch-pulse {
  animation: cam-chip-pulse 1.2s ease-in-out infinite;
}
@keyframes cam-chip-pulse {
  0%,
  100% {
    background: rgba(255, 255, 255, 0.15);
  }
  50% {
    background: rgba(255, 255, 255, 0.35);
  }
}
/* Action-specific chip colors */
.cam-chip.ch-add {
  background: rgba(34, 197, 94, 0.4);
}
.cam-chip.ch-move {
  background: rgba(59, 130, 246, 0.4);
}
.cam-chip.ch-remove {
  background: rgba(239, 68, 68, 0.4);
}
.cam-chip.ch-harvest {
  background: rgba(245, 158, 11, 0.4);
}

.cam-hud-btns {
  display: flex;
  gap: 10px;
  align-items: center;
}
.cam-hud-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: max(44px, var(--tap-min));
  height: max(44px, var(--tap-min));
  border-radius: 50%;
  border: none;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  cursor: pointer;
  transition: background 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.cam-hud-btn:active {
  background: rgba(255, 255, 255, 0.3);
}
/* Ohne diese Zeile wirkte `hidden` hier nicht: die display-Angabe oben ist eine
   Autoren-Regel und schlägt die [hidden]-Regel des Browsers (dasselbe wie bei
   .btn[hidden]). Die Taschenlampe stand darum auf jedem Telefon da, auch auf
   denen ohne Lampe, wo ein Tipp darauf nichts tut. */
.cam-hud-btn[hidden] {
  display: none;
}
/* Torch toggle — visible glow when the flashlight is on so the worker doesn't
   leave the camera flash burning unintentionally after a scan. */
.cam-hud-btn.torch-on {
  background: rgba(250, 204, 21, 0.45);
  color: #fef9c3;
  box-shadow: 0 0 16px rgba(250, 204, 21, 0.5);
}
.cam-hud-btn.torch-on:active {
  background: rgba(250, 204, 21, 0.6);
}

/* Die Serien-Vorgänge als Kacheln: Umziehen, Ernten, Kontamination, Entsorgen,
   Verbraucht. Ein Tipp, dann Beutel für Beutel scannen — so einfach muss es
   sein, sonst benutzt es niemand. Sie tun etwas anderes als Lampe und
   Rückgängig in der Reihe darunter: sie ändern, was der nächste Scan tut, und
   das muss man lesen können, ohne es zu kennen. Darum mit Wort, und darum nicht
   in der Werkzeugreihe — dort war schon für sechs runde Knöpfe zu 56px kein
   Platz (386px auf 358px). Zwei Kacheln je Reihe, damit auch "Kontamination"
   ganz dasteht.

   Das Raster hat vier Spalten, und jede Kachel nimmt zwei: so steht die fünfte
   allein in der dritten Reihe mittig und so breit wie die anderen, statt links
   mit einem Loch daneben oder als Balken über die ganze Breite. */
.cam-hud-modes {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
  width: 100%;
  max-width: 380px;
}
.cam-hud-modes > .cam-mode-btn {
  grid-column: span 2;
}
.cam-hud-modes > .cam-mode-btn:last-child:nth-child(odd) {
  grid-column: 2 / span 2;
}
.cam-mode-btn {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-width: 0;
  min-height: max(44px, var(--tap-min));
  padding: 0 12px;
  border: none;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  font-family: inherit;
  font-size: max(0.875rem, var(--fs-min));
  font-weight: 600;
  cursor: pointer;
  transition:
    background 0.15s,
    box-shadow 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.cam-mode-btn[hidden],
.cam-mode-btn [hidden] {
  display: none;
}
.cam-mode-btn svg {
  flex: none;
}
.cam-mode-btn:active {
  background: rgba(255, 255, 255, 0.3);
}
/* Ein langer Zielname wird gekürzt statt umgebrochen; ganz steht er in der
   Meldung nach jedem Scan. */
.cam-mode-l {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* An leuchtet jeder Knopf in seiner Farbe. Ein Vorgang, der unbemerkt
   weiterläuft, gehorcht dem nächsten Scan stumm — das ist bei der Lampe so und
   hier erst recht, also muss "an" von weitem zu sehen sein. */
.cam-mode-btn.cam-mode-move.on {
  background: #1d4ed8;
  color: #fff;
  box-shadow: 0 0 16px rgba(37, 99, 235, 0.6);
}
.cam-mode-btn.cam-mode-move.on:active {
  background: #1e40af;
}
.cam-mode-btn.cam-mode-harvest.on {
  background: var(--c-amber-strong);
  color: #fff;
  box-shadow: 0 0 16px rgba(217, 119, 6, 0.6);
}
/* CONTAM toggle. Armed, every bag scanned opens a contamination report, so it
   has to be as obvious as the torch that it is on — the failure mode is the
   same shape: a mode left running that the next scan silently obeys. */
.cam-mode-btn.cam-mode-contam.on {
  background: rgba(220, 38, 38, 0.55);
  color: #fff;
  box-shadow: 0 0 16px rgba(220, 38, 38, 0.55);
}
.cam-mode-btn.cam-mode-contam.on:active {
  background: rgba(220, 38, 38, 0.7);
}
/* Raus hell auf dunkel: eine vierte Farbe wäre zu nah am Rot der
   Kontamination, und die zwei darf man nicht verwechseln. */
.cam-mode-btn.cam-mode-trash.on {
  background: #f5f5f4;
  color: #1c1917;
  box-shadow: 0 0 16px rgba(255, 255, 255, 0.45);
}
.cam-mode-move .cam-hud-badge {
  color: #1d4ed8;
}
.cam-mode-harvest .cam-hud-badge {
  color: var(--c-amber-strong);
}
.cam-mode-trash .cam-hud-badge {
  background: #1c1917;
  color: #fff;
}
/* Zähler auf den Serien-Knöpfen: wieviele Beutel der laufende Durchgang schon
   hat (bei der Kontamination: gesammelt). Die Zahl sitzt als eigene Scheibe an
   der Ecke. */
.cam-hud-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  border-radius: 10px;
  background: #fff;
  color: #b91c1c;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  pointer-events: none;
}
.cam-hud-badge[hidden] {
  display: none;
}
.cam-hud-done {
  background: #16a34a;
}
.cam-hud-done:active {
  background: #15803d;
}

/* Camera button in scan modal */
.btn-scan-cam {
  display: none;
}
@media (pointer: coarse) and (hover: none) {
  .btn-scan-cam {
    display: inline-flex;
    align-items: center;
    gap: 4px;
  }
}

/* ═══ CAMERA FAB (mobile/tablet only) ════════════════════════ */
/* Hidden by default on desktop (pointer:fine = mouse) */
.cam-fab {
  display: none;
  position: fixed;
  bottom: calc(var(--fab-bottom) + env(safe-area-inset-bottom, 0px));
  right: calc(24px + env(safe-area-inset-right, 0px));
  z-index: 850;
  gap: 9px;
  height: 56px;
  padding: 0 22px;
  border-radius: 28px;
  border: none;
  background: var(--c-primary, #16a34a);
  color: #fff;
  font-size: 1rem;
  font-weight: 600;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  cursor: pointer;
  align-items: center;
  justify-content: center;
  transition:
    transform 0.15s,
    box-shadow 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.cam-fab-label {
  white-space: nowrap;
}
.cam-fab:active {
  transform: scale(0.92);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
/* Show only on touch devices (coarse pointer = finger/stylus, no hover = no mouse) */
@media (pointer: coarse) and (hover: none) {
  .cam-fab {
    display: flex;
  }
}

/* -- Harvest panel -- */
#harvest-panel {
  padding: 12px 24px;
  background: #fffbeb;
  border-bottom: 1px solid #fde68a;
  display: none;
}
#harvest-panel .inner {
  max-width: 980px;
  margin: 0 auto;
  display: flex;
  gap: 12px;
  align-items: flex-end;
  flex-wrap: wrap;
}

/* -- Blockbuch: Gut-Knopf, Notizen, Liste -- */
.block-good {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.block-good[hidden] {
  display: none;
}
.block-good[aria-pressed='true'] {
  background: var(--c-amber-light);
  border-color: var(--c-amber-dark);
  color: var(--c-amber-dark);
  font-weight: 600;
}
.block-good[aria-pressed='true'] svg {
  fill: currentColor;
}
#bi-good {
  margin-left: auto;
  margin-right: 8px;
}
.bb-bi-notes {
  margin-top: 4px;
}
.bb-bi-notes textarea {
  width: 100%;
  margin-top: 6px;
  resize: vertical;
}
.bb-bi-save {
  margin-top: 6px;
}
.bb-note {
  padding: 6px 0;
  border-bottom: 1px solid var(--c-border);
}
.bb-note:last-child {
  border-bottom: 0;
}
.bb-note-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.bb-note-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--c-text-muted);
  margin-top: 2px;
}
.bb-note-del {
  margin-left: auto;
  min-width: var(--tap);
  border: 0;
  background: transparent;
  color: var(--c-text-muted);
  cursor: pointer;
  font-size: 1.125rem;
  line-height: 1;
}
.bb-entry,
.bb-sum {
  padding: 10px 0;
  border-bottom: 1px solid var(--c-border);
}
.bb-entry:last-child,
.bb-sum:last-child {
  border-bottom: 0;
}
.bb-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  margin-bottom: 4px;
}
.bb-harvest {
  margin-left: auto;
  color: var(--c-amber-dark);
}
.bb-star {
  fill: var(--c-amber-dark);
}
.bb-line {
  margin-top: 2px;
  overflow-wrap: anywhere;
}
.bb-mono {
  font-family: monospace;
}
.bb-muted {
  color: var(--c-text-muted);
}
.bb-notes {
  margin-top: 6px;
  padding-left: 10px;
  border-left: 2px solid var(--c-amber-border);
}
.bb-sum-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 8px;
}

/* ═══ LAYOUT ════════════════════════════════════════════════════ */
.main {
  padding: var(--pad-page);
  max-width: 1400px;
  margin: 0 auto;
  /* Regel R2: der Kasten misst, nicht das Fenster.
     Alles, was in dieser Spalte steht, kann von hier ab nach IHRER Breite
     fragen statt nach der des Fensters — und das sind zwei verschiedene
     Zahlen, sobald die Seitenleiste da ist: bei 769 px Fenster bleiben
     769 − 240 − 48 = 481 px übrig. Genau diese Lücke war die Klippe bei
     769 px, an der zwei Ratschenzeilen hingen.

     Der Name ist nötig und nicht Zierde: ohne ihn fragt eine
     Container-Abfrage den NÄCHSTEN Vorfahren mit container-type, und wer
     später einen zweiten Kasten dazwischensetzt, verschiebt lautlos, worauf
     sich jede dieser Regeln bezieht.

     Was das sonst noch tut, ist gemessen und nicht angenommen:
     `container-type` macht `.main` in keiner der beiden Maschinen zum
     Bezugsrahmen für `position: fixed` (P0, maschinen/), und dass nichts
     darin an einen anderen Platz rutscht, halten 142 Bilder vorher/nachher
     aus `--shots`. */
  container-type: inline-size;
  container-name: inhalt;
}
.page {
  display: none;
}
.page.active {
  display: block;
}
.card {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: var(--pad-card);
  margin-bottom: 14px;
  box-shadow: var(--shadow);
}
/* A .sec heading on the left, controls on the right — twenty-three of these in
   index.html, all spelled out inline, and eighteen without a wrap. On a desktop
   the wrap never fires because everything fits; at 375px the culture library's
   two filters and its Check button came to 366px beside a 101px heading and
   pushed the whole page to 467. Measured, not guessed: scripts/measure-mobile.js.

   The tools group wraps too. Wrapping only the outer row moved the page from
   467 to 399 and left it still overflowing, because the group by itself is
   wider than the card's 311px content box.

   Named for the heading it carries and not the card it sits in, because
   `.card-head` is already the illustrated header the work tiles use — same
   words, unrelated job, and two rules with one name would have fought over
   display, padding and margin at a distance of 4500 lines. */
.sec-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}
.sec-head .sec {
  margin: 0;
}
.sec-head-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
/* The size utilities, and why this app has three of them when it otherwise
   names things after what they are.

   §10 of MOBILE_REDESIGN.md draws the line: colours, widths and margins stay
   inline, only the font sizes come out. That is not squeamishness — the size
   is the one inline property that has a token, and it is the only one that was
   wrong on a phone. So these three are the token layer reaching markup that a
   stylesheet could not otherwise touch, not the start of a utility system.

   Each is a pair: the floor in a hand, the number that was already there on a
   desk. --fs-meta 13/12, --fs-xs 13/11, --fs-micro 13/10.

   They are what lets the §6 bridge go. That block matches `[style*=
   'font-size:11px']` and lifts it with !important, which works and costs an
   attribute-substring match against every element in the document; a class
   costs nothing and can be read in the markup.

   The class name is doubled, and that is the load-bearing part. An inline
   style outranks every normal rule, so moving one onto a plain class quietly
   hands the element back to whatever component rule it was overriding. That
   is not hypothetical: the first version of this measured thirteen elements
   changing size at 1440px — four hints under `.modal p` going 12px to 14px,
   six buttons under `.btn-sm` going 11px to 12px. Doubling reaches (0,2,0),
   which outranks the descendant and component rules these replace.

   !important would also have worked and is deliberately not used: the iOS
   anti-zoom rule below is `input, select, textarea { font-size: 16px
   !important }` at (0,0,3), and an important utility at (0,1,0) would beat it
   — handing Safari a 13px field to zoom into on every form control that used
   to carry an inline size. A doubled class loses to it, which is correct. */
/* A <canvas> with no width attribute is 300px wide, and that is wider than a
   card's content box on a 320px phone — measured at 320, where two harvest
   charts hang 13px off the edge and take the page to 333.

   Honest about what that measurement is: the page it was taken on has its
   scripts stripped, so Chart.js never ran, and Chart.js in responsive mode
   sizes the canvas to its container itself. So this is the pre-initialisation
   state rather than proof of a permanent defect. It is still worth a rule —
   it covers the flash before the first draw, and any container Chart.js cannot
   measure — and one line is cheaper than finding out which.

   The one canvas that is not a chart — #pa-canvas, the photo annotation
   surface — was going to be excluded from this, until a check showed it sets
   max-width: 100% for itself two thousand lines further down. So there is
   nothing to exclude, and the rule says the plain thing. */
canvas {
  max-width: 100%;
}

/* Ein `<summary>` klappt auf und zu; es ist ein Bedienelement, auch wenn es wie
   eine Beschriftung aussieht. Das von `#nb-sub-section` war 18 px hoch und
   damit am Rechner unter den 24 px der WCAG 2.5.8. Der statische Lauf des
   Messstands hat es gefunden, der laufende nie: dort steckt es in einem
   zugeklappten Formular und wird nie gemessen. */
summary.fs-meta {
  min-height: var(--tap-sm);
  display: flex;
  align-items: center;
}
/* The two steps at the top of the ladder, added on 2026-08-23 for the inline
   sizes in app.js. 13px and 15px are the two commonest numbers there by a long
   way -- 26 and 8 of the 44 -- and both are already a mark's desktop value, so
   the class changes nothing on a desk and gives the phone the 15 and 17 the
   token layer decided on. */
.fs-sm.fs-sm {
  font-size: var(--fs-sm);
}
.fs-base.fs-base {
  font-size: var(--fs-base);
}
.fs-meta.fs-meta {
  font-size: var(--fs-meta);
}
.fs-xs.fs-xs {
  font-size: var(--fs-xs);
}
.fs-micro.fs-micro {
  font-size: var(--fs-micro);
}
/* And a fourth for the sizes that are not on the scale at all: 10.5px on a
   fruiting target, 11.5px on a week strip, 9px on a bag chip, 8px on a bag
   label. Sixteen of them across eight components, no two the same, none of
   them a considered step — they are what somebody typed to make a row fit.

   Rounding them to the nearest of the three above would have moved sixteen
   desktop values by up to 2px to save a class, and "the desktop does not move"
   is the promise this whole project rests on. So the number stays where it
   was, on the element, as a custom property — and gains the floor through the
   same max() the base rules use. `--fs-own: 10.5px` reads 10.5px on a desk and
   13px in a hand, and the ratchet stops counting it because it is no longer a
   font-size the cascade cannot reach. */
.fs-floor.fs-floor {
  /* The fallback is not decoration. `var(--fs-own)` with nothing to resolve is
     invalid at computed-value time, which does not fall back to the previous
     declaration — it makes the element inherit, silently, at whatever size its
     parent happens to be. A class whose whole job is to pin a size would then
     do the opposite of its job. --fs-xs is the nearest thing to "the size you
     probably meant", and an assertion in test/mobile-tokens.test.js fails if
     any element wearing this class arrives without its number. */
  font-size: max(var(--fs-own, var(--fs-xs)), var(--fs-min));
}

/* A filter that sizes to its own content rather than the form grid. */
.head-select {
  width: auto;
  font-size: var(--fs-meta);
  padding: 4px 8px;
}
.sec {
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-text-muted);
  margin-bottom: 10px;
}

/* -- Sub-tabs -- */
.stabs {
  display: flex;
  gap: 4px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--c-border);
  padding-bottom: 10px;
}
.stab {
  padding: var(--pad-stab);
  /* --tap-sm, not --tap: these are secondary navigation, and Phase 1 replaces
     the strip with a drill-down list on a phone anyway. 56px pills would be
     enormous for something on its way out. */
  min-height: var(--tap-sm);
  border-radius: 99px;
  font-size: var(--fs-sm);
  font-weight: 500;
  cursor: pointer;
  border: none;
  background: transparent;
  color: var(--c-text-sec);
  font-family: inherit;
  transition: all 0.15s;
}
.stab.active {
  background: var(--c-primary);
  color: #fff;
}
.stab:hover:not(.active) {
  background: var(--c-border-light);
}
.sp {
  display: none;
}
.sp.active {
  display: block;
}
/* The back row belongs to the drill-down below; on a desktop there is nothing
   to go back from, so it never appears. */
.stab-back {
  display: none;
}

/* ── Sub-tabs on a phone: a drill-down, not a strip ────────────────────────
   A row of pills has no good answer to "there are thirteen of them". The two
   it had were both treatments of the symptom: wrap to a second line on the
   work pages, sideways scroll with a fade edge on Admin — the first eats a
   third of a short screen, the second hides the far end from anyone who does
   not think to swipe a thing that does not look swipeable.

   So below 769px the strip is the page's index: full width, one per line,
   chevron on the right. Picking one shows that sub-page alone, with a back
   row above it. `stab-drilled` on the .page element is which of the two
   states the page is in; above 769px nothing reads the class and the pills
   are untouched.

   Landing goes straight to the sub-page the page defaults to, so the common
   case — Chargen opens on the batch list — still costs no tap. The one page
   that opens on its index instead falls out of the same rule rather than a
   name: Admin's default tab is display:none, so there is no sub-page to land
   on, and the list is what you get. */
/* zwilling: untertableiste */
@media (max-width: 768px) {
  .stabs {
    flex-direction: column;
    gap: 0;
    padding-bottom: 0;
    margin-bottom: 12px;
    border-bottom: none;
  }
  .stab {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    /* --tap, not --tap-sm: on a phone this is the page's navigation, which is
       the argument the base rule makes for keeping it small in reverse. */
    min-height: var(--tap);
    border-radius: 0;
    border-bottom: 1px solid var(--c-border-light);
    text-align: left;
    font-size: var(--fs-base);
    color: var(--c-text);
  }
  /* A full-width green bar for "you were last here" would read as the page's
     primary action. In a list, the mark is the label. */
  .stab.active {
    background: transparent;
    color: var(--c-primary);
    font-weight: 600;
  }
  .stab:hover:not(.active) {
    background: transparent;
  }
  /* Drawn, not typed: a › from the font renders at a different size and
     baseline in every fallback, and this one has to line up with the others. */
  .stab::after {
    content: '';
    flex-shrink: 0;
    width: 8px;
    height: 8px;
    margin-right: 4px;
    border-right: 2px solid var(--c-text-muted);
    border-bottom: 2px solid var(--c-text-muted);
    transform: rotate(-45deg);
  }
  .stab.active::after {
    border-color: var(--c-primary);
  }
  .page.stab-drilled > .stabs {
    display: none;
  }
  .page.stab-drilled > .stab-back {
    display: flex;
  }
  .stab-back {
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: var(--tap);
    padding: 0 4px;
    margin-bottom: 12px;
    border: none;
    border-bottom: 1px solid var(--c-border-light);
    background: transparent;
    color: var(--c-primary);
    font-family: inherit;
    font-size: var(--fs-sm);
    font-weight: 600;
    text-align: left;
    cursor: pointer;
  }
  .stab-back-ico {
    font-size: 1.25rem;
    line-height: 1;
  }
  /* On the index there is no sub-page. The .active panel keeps its class, so
     going back into it needs no state of its own — this rule outranks
     `.sp.active` on specificity alone (0,4,0 against 0,2,0).

     `.stab-drill` is not decoration: it is added by stabDrillInit(), so this
     reaches only the pages that were given a back row — any page whose strip is
     hidden is skipped there and would otherwise render blank on a phone, every
     sub-page hidden and no index to reach them by. (Bestellungen was that page
     until its five views stopped being sidebar entries.) It also means the whole
     mechanism is inert if the script does not run, which is the right way round. */
  .page.stab-drill:not(.stab-drilled) .sp {
    display: none;
  }
}

/* zwilling: untertableiste
   Regel R2, dieselbe Rechnung wie beim Kartenmodus: 768 − 2×16 = 736. Die
   Leiste wird zur Liste, sobald IHRE Spalte schmal ist, nicht wenn das
   Fenster es ist. Der Kontaminationsstreifen des Labors hing zwischen 769
   und 800 px bis zu 33 px über den Rand, weil er bei 769 px die Breite des
   Fensters annahm, während die Seitenleiste ihm 240 px davon genommen hatte.

   Zweimal geschrieben wie der Kartenmodus, und aus demselben Grund; ein Test
   hält die beiden Fassungen gleich. */
@container inhalt (max-width: 736px) {
  .stabs {
    flex-direction: column;
    gap: 0;
    padding-bottom: 0;
    margin-bottom: 12px;
    border-bottom: none;
  }
  .stab {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    /* --tap, not --tap-sm: on a phone this is the page's navigation, which is
       the argument the base rule makes for keeping it small in reverse. */
    min-height: var(--tap);
    border-radius: 0;
    border-bottom: 1px solid var(--c-border-light);
    text-align: left;
    font-size: var(--fs-base);
    color: var(--c-text);
  }
  /* A full-width green bar for "you were last here" would read as the page's
     primary action. In a list, the mark is the label. */
  .stab.active {
    background: transparent;
    color: var(--c-primary);
    font-weight: 600;
  }
  .stab:hover:not(.active) {
    background: transparent;
  }
  /* Drawn, not typed: a › from the font renders at a different size and
     baseline in every fallback, and this one has to line up with the others. */
  .stab::after {
    content: '';
    flex-shrink: 0;
    width: 8px;
    height: 8px;
    margin-right: 4px;
    border-right: 2px solid var(--c-text-muted);
    border-bottom: 2px solid var(--c-text-muted);
    transform: rotate(-45deg);
  }
  .stab.active::after {
    border-color: var(--c-primary);
  }
  .page.stab-drilled > .stabs {
    display: none;
  }
  .page.stab-drilled > .stab-back {
    display: flex;
  }
  .stab-back {
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: var(--tap);
    padding: 0 4px;
    margin-bottom: 12px;
    border: none;
    border-bottom: 1px solid var(--c-border-light);
    background: transparent;
    color: var(--c-primary);
    font-family: inherit;
    font-size: var(--fs-sm);
    font-weight: 600;
    text-align: left;
    cursor: pointer;
  }
  .stab-back-ico {
    font-size: 1.25rem;
    line-height: 1;
  }
  /* On the index there is no sub-page. The .active panel keeps its class, so
     going back into it needs no state of its own — this rule outranks
     `.sp.active` on specificity alone (0,4,0 against 0,2,0).

     `.stab-drill` is not decoration: it is added by stabDrillInit(), so this
     reaches only the pages that were given a back row — any page whose strip is
     hidden is skipped there and would otherwise render blank on a phone, every
     sub-page hidden and no index to reach them by. (Bestellungen was that page
     until its five views stopped being sidebar entries.) It also means the whole
     mechanism is inert if the script does not run, which is the right way round. */
  .page.stab-drill:not(.stab-drilled) .sp {
    display: none;
  }
}

/* ═══ ORDER HUB ═════════════════════════════════════════════════ */
.oh-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--c-amber-light);
  border: 1px solid var(--c-amber-border);
  color: var(--c-amber-dark);
  border-radius: var(--radius);
  padding: 10px 14px;
  margin-bottom: 12px;
  font-size: 0.8125rem;
}
.oh-ch {
  display: inline-block;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 700;
  border-radius: 6px;
  padding: 1px 7px;
  white-space: nowrap;
}
.oh-ch-wix {
  background: var(--c-blue-light);
  color: var(--c-blue-dark);
}
.oh-ch-etsy {
  background: var(--c-amber-light);
  color: var(--c-amber-dark);
}
.oh-ch-ebay {
  background: var(--c-indigo-light);
  color: var(--c-indigo-dark);
}
.oh-ch-manual {
  background: var(--c-border-light);
  color: var(--c-text-sec);
}
.oh-ch-shopify {
  background: var(--c-green-light);
  color: var(--c-green-dark);
}
.oh-st {
  display: inline-block;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  border-radius: 999px;
  padding: 2px 9px;
  white-space: nowrap;
}
.oh-st-new {
  background: var(--c-blue-light);
  color: var(--c-blue-dark);
}
.oh-st-in_production {
  background: var(--c-amber-light);
  color: var(--c-amber-dark);
}
.oh-st-ready {
  background: var(--c-green-light);
  color: var(--c-green-dark);
}
.oh-st-shipped {
  background: var(--c-border-light);
  color: var(--c-text-muted);
}
.oh-st-cancelled {
  background: var(--c-red-light);
  color: var(--c-red-dark);
}
/* Overbooked pickup slot: the same red as a cancellation, under its own name.
   Borrowing .oh-st-cancelled would put "cancelled" in the markup for something
   that is the opposite of cancelled — more orders than the slot holds. */
.oh-st-over {
  background: var(--c-red-light);
  color: var(--c-red-dark);
}
.oh-fchip {
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 600;
  border: 1px solid var(--c-border);
  background: var(--c-surface);
  border-radius: 999px;
  padding: 5px 12px;
  cursor: pointer;
  color: var(--c-text-sec);
  /* 27.5px, and there are four of them in a row: the smallest targets left in
     the app after the chrome pass. A pill's whole job is to be tapped. */
  min-height: var(--tap-sm-min);
}
/* The worklist dialog's "group by" pair. Spelled out inline on both buttons,
   nine declarations each and identical apart from the label, which is why
   neither could pick up a touch floor: there was nothing to put it on. */
.wl-group-btn {
  flex: 1;
  padding: 9px 10px;
  border: 1px solid var(--c-border);
  border-radius: 9px;
  background: transparent;
  /* --fs-sm and not --fs-xs: the inline value was a flat 13px, and --fs-xs is
     11px on a desktop, so the obvious-looking token would have shrunk it
     there. --fs-sm is 13px on a desktop — unchanged — and 15px in a hand. */
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
  min-height: var(--tap-sm-min);
}
.oh-fchip.active {
  background: var(--c-primary);
  border-color: var(--c-primary);
  color: #fff;
}
/* Packen (Bestellungen → Packen): one card per parcel, read on a phone at the
   packing table. Cards rather than table rows, so nothing scrolls sideways. */
.ob-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.ob-count {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  white-space: nowrap;
}
.ob-hint {
  margin-bottom: 8px;
}
.ob-sync {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 12px;
  font-size: var(--fs-sm);
  padding: 8px 12px;
  margin-bottom: 12px;
  border-radius: var(--radius);
  background: var(--c-surface);
  border: 1px solid var(--c-border-light);
}
.ob-sync.is-warn {
  background: var(--c-amber-light);
  border-color: var(--c-amber-border);
  color: var(--c-amber-dark);
}
.ob-empty {
  padding: 14px 4px;
  color: var(--c-text-muted);
  font-size: var(--fs-sm);
}
.ob-order {
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  padding: 10px 12px;
  margin-bottom: 8px;
}
.ob-order.is-packed {
  background: var(--c-bg);
  border-style: dashed;
}
.ob-order.is-packed .ob-items {
  color: var(--c-text-muted);
}
.ob-customer {
  font-weight: 700;
  font-size: var(--fs-sm);
  margin-bottom: 4px;
  overflow-wrap: anywhere;
}
.ob-line-sku {
  font-family: monospace;
  font-weight: 600;
  overflow-wrap: anywhere;
}
/* Alles zusammen: one row per product, the count big enough to read across a
   table. */
.ob-total {
  border-bottom: 1px solid var(--c-border-light);
  padding: 8px 0;
}
.ob-total:last-child {
  border-bottom: none;
}
.ob-total-top {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
}
.ob-total-n {
  font-size: 1.5rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  min-width: 2ch;
}
.ob-total-name {
  font-family: monospace;
  font-weight: 700;
  font-size: var(--fs-sm);
}
.ob-total-orders {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
.ob-sizes {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 4px;
}
.ob-size {
  font-size: var(--fs-sm);
  font-weight: 600;
  border-radius: 999px;
  padding: 2px 10px;
  background: var(--c-primary-light);
  color: var(--c-text);
}
.ob-total-skus {
  margin-top: 4px;
  font-family: monospace;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  overflow-wrap: anywhere;
}
.ob-order-top {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin-bottom: 6px;
}
.ob-num {
  font-family: monospace;
  font-weight: 700;
  font-size: var(--fs-sm);
}
.ob-when {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
.ob-link {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--c-primary);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-sm-min);
}
.ob-items {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--fs-sm);
}
.ob-items li {
  padding: 3px 0;
}
.ob-qty,
.ob-make-qty {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.ob-sku {
  font-family: monospace;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
.ob-tag {
  display: inline-block;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  border-radius: 999px;
  padding: 1px 8px;
  white-space: nowrap;
}
.ob-tag-make {
  background: var(--c-amber-light);
  color: var(--c-amber-dark);
}
.ob-order-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 6px 10px;
  margin-top: 8px;
}
.ob-plan {
  margin-right: auto;
  font-size: var(--fs-xs);
  color: var(--c-amber-dark);
  font-weight: 600;
}
.ob-done {
  font-size: var(--fs-sm);
  color: var(--c-green-dark);
  font-weight: 600;
}
.ob-group {
  border-left: 4px solid var(--c-primary);
  padding-left: 8px;
  margin-bottom: 10px;
}
.ob-group-head,
.ob-combine-solo {
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--c-primary);
  margin-bottom: 6px;
}
.ob-make {
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-bottom: 8px;
}
.ob-make.is-late {
  border-color: var(--c-red-border);
}
.ob-make-top {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: var(--fs-sm);
}
.ob-make-qty {
  font-size: 1.25rem;
}
.ob-make-meta {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  margin-top: 2px;
}
.ob-make-days {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}
.ob-day {
  font-size: var(--fs-xs);
  border-radius: 999px;
  padding: 2px 9px;
  background: var(--c-amber-light);
  color: var(--c-amber-dark);
}
.ob-day.is-late {
  background: var(--c-red-light);
  color: var(--c-red-dark);
}
.ob-done-list > summary {
  cursor: pointer;
  min-height: var(--tap-sm-min);
  align-items: center;
}
/* Billbee's state on the card: information, never what takes an order off the
   list. A bought label is the loud one, because that parcel leaves today. */
.ob-bb {
  display: inline-block;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  border-radius: 999px;
  padding: 1px 8px;
  white-space: nowrap;
  background: var(--c-border-light);
  color: var(--c-text-sec);
}
.ob-bb-label,
.ob-bb-shipped {
  background: var(--c-blue-light);
  color: var(--c-blue-dark);
}
.ob-plan.is-due {
  color: var(--c-red-dark);
}
.oh-manual {
  background: var(--c-bg);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  padding: 12px;
  margin-bottom: 10px;
}
.oh-maprow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--c-border-light);
}
.oh-maprow:last-child {
  border-bottom: none;
}
.oh-warn {
  color: var(--c-amber-dark);
  cursor: help;
}

/* ═══ FORMS ═════════════════════════════════════════════════════ */
label {
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 500;
  color: var(--c-text-sec);
  display: block;
  margin-bottom: 4px;
}
/* A <summary> is a button that does not look like one, and every one in this
   app was the height of its own line — 19.5px. Floored generically rather than
   per instance, because the next <details> someone writes has the same problem
   and no reason to know about it.

   min-height and not display:flex, deliberately: flex on a summary drops the
   disclosure triangle in every engine, and the triangle is the only thing that
   says the row opens. align-content centres the text inside the taller box on
   anything from 2024 on; where it is not supported the text sits at the top of
   the box, which is untidy and still hits the floor — the target is correct
   either way, which is the half that matters. */
summary {
  min-height: var(--tap-sm-min);
  align-content: center;
}
input,
select,
textarea {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  background: var(--c-surface);
  color: var(--c-text);
  font-family: inherit;
  transition:
    border-color 0.15s,
    box-shadow 0.15s;
  resize: vertical;
  min-height: 64px;
  line-height: 1.5;
}

/* The anti-zoom floor, and it moved axis on 2026-08-23 rather than moving
   value. iOS Safari zooms the whole page in when a focused field's text is
   under 16 CSS px and never zooms back out; the way back is a pinch the user
   has to know about.

   It used to live inside `@media (max-width: 768px)`, which is rule R1 broken
   in the plainest way there is: a WIDTH deciding a TOUCH behaviour. A tablet
   in landscape is 1024px wide and still has a finger, so from 769px up the
   rule stopped applying and 26 fields across nine pages went back to 13 and
   14px — measured, not argued: measure-mobile.js --app reported every one of
   them starting at exactly 769px.

   `any-pointer` and not `pointer`, for the same reason the shop's axis rules
   use it: a touchscreen laptop reports `pointer: fine`, because its PRIMARY
   pointer is the trackpad, and `any-pointer: coarse` alongside. It is the
   device most likely to be carried into a growing room, and `pointer: coarse`
   is exactly the spelling that misses it.

   max() and not a flat 16px, because the two halves say different things.
   `1rem` is the reader's own setting and must be allowed to grow; 16px is
   WebKit's threshold and must never shrink below it, whatever the reader set.
   A flat 16px would throw a reader who chose 20px back down to 16.

   !important because .fs-meta is (0,2,0) against this selector's (0,0,3), and
   .fs-meta is what the compact filter and search fields wear since the inline
   sizes were cleaned up. Without it Safari gets a 13px field to zoom into on
   exactly the pages that were tidied. */
@media (any-pointer: coarse) {
  input,
  select,
  textarea {
    font-size: max(1rem, 16px) !important;
  }
}
input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--c-primary);
  box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.1);
}
/* The floor, applied to the element rather than to a list of classes.

   §5.1 gave the phone a 56px touch target and the token layer delivered it, but
   only the controls somebody remembered to give a class consumed it: .btn,
   .stab, .sb-btn, the bottom nav. Twenty-five buttons had no height rule at
   all and came out at whatever their padding made — the Woche/Monat/Jahr
   toggle at roughly 28px, "alles gelesen" at 20px, the pack-size delete at 20.
   Patching those twenty-five leaves the twenty-sixth to be written next week,
   so the rule goes where a button cannot avoid it.

   `button` is (0,0,1), so every class in this sheet still outranks it — .btn
   keeps its 56px --tap, .stab its 48px --tap-sm — and this only reaches the
   controls nothing else spoke for. --tap-sm rather than --tap because that is
   what a control of unknown importance should get, and because it is `auto` on
   a desk: the desktop cannot move here, which the baseline fixture checks. */
button {
  min-height: var(--tap-sm);
}
/* `hidden` muss auch auf Knöpfen wirken. Die display-Angabe unten ist eine
   Autoren-Regel und schlägt damit die [hidden]-Regel des Browsers; ohne diese
   Ausnahme bleibt ein Knopf mit hidden sichtbar (siehe .worktree-banner). */
.btn[hidden] {
  display: none;
}
.btn {
  padding: var(--pad-btn);
  /* Eine Tippfläche hat ZWEI Maße. Gemessen bei 900 px mit der Maus: der
     Schließknopf im Labor war 23 x 24 px, also hoch genug und trotzdem unter
     der Marke. `min-height` allein deckt jeden Knopf mit Text und keinen mit
     einem einzelnen Zeichen darauf. */
  min-height: var(--tap);
  min-width: var(--tap);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--c-border);
  background: var(--c-surface);
  color: var(--c-text);
  font-family: inherit;
  transition: all 0.15s;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.btn:hover {
  background: var(--c-bg);
}
.btn-p,
.btn-green {
  background: var(--c-primary);
  color: #fff;
  border-color: transparent;
}
.btn-p:hover,
.btn-green:hover {
  background: var(--c-primary-hover);
}
/* Die eine Handlung eines Abschnitts, über seinen Reitern.
   Größer als ein gewöhnlicher Knopf durch Polsterung, Schrift und Symbol —
   nicht durch Breite. Ein Knopf, der die Zeile ausfüllt, liest sich als Banner
   und nicht als etwas zum Drücken, und volle Breite ist ohnehin keine Größe,
   die jemand gewählt hat. Groß genug muss nur heißen: nicht zu verwechseln mit
   den Reiter-Pillen darunter, die die Ansicht wechseln statt etwas anzulegen. */
.btn-lead {
  gap: 8px;
  padding: 11px 20px;
  font-size: max(0.875rem, var(--fs-min));
  font-weight: 600;
  margin-bottom: 10px;
}
/* Ein Formular ist so breit wie seine Felder, nicht so breit wie das Fenster.
   Die beiden Karten, die das hier tragen, standen bis vor kurzem nebeneinander
   in einem Zwei-Spalten-Raster und waren für dessen halbe Breite gebaut; als
   das Raster wegfiel, erbten sie die ganze Zeile — vier kurze Felder über
   1400px. Dieselbe Sache wie beim Knopf darüber und bei der Hinweiskarte davor. */
.form-card {
  max-width: 640px;
}
.btn-r {
  background: #fff5f5;
  color: #b91c1c;
  border-color: #fecaca;
}
.btn-r:hover {
  background: #fee2e2;
}
/* A width the desk caps and a phone does not. The number arrives as data on the
   element (`--w-cap: 480px`) rather than as an inline `max-width` declaration,
   because an inline declaration wins against every rule in this file — including
   the phone rule that exists to undo exactly this cap. */
/* The field a validation message is about. Not a colour alone — a 3px ring, so
   it survives a red/green colour deficiency and a bright greenhouse — and it is
   removed on the next keystroke by fieldError() rather than on submit, because a
   ring that outlives the mistake makes a field look broken while it is being
   fixed. */
input.is-invalid,
select.is-invalid,
textarea.is-invalid {
  border-color: var(--c-red);
  box-shadow: 0 0 0 3px var(--c-red-light);
}

.w-cap {
  max-width: var(--w-cap);
}

.btn-sm {
  padding: 5px 10px;
  font-size: max(0.75rem, var(--fs-min));
}
/* Touch-device tap targets — covers phones AND tablets in landscape, which the
   old (max-width:768px) breakpoint missed. Applies to every .btn-xs / .btn-sm
   site-wide so post-scan actions, scan-modal controls, and table action columns
   all clear the 44px minimum. */
/* Two literals here quietly outranked the token layer: this block is later than
   `.btn` at the same specificity, so 44px beat var(--tap) and 14px beat
   var(--fs-sm) on every coarse pointer — which is every device the tokens were
   written for. It never applies while the baseline is captured (the fixture
   records minHeight 0px for .btn.btn-sm), so saying it in tokens moves nothing. */
@media (pointer: coarse) {
  .btn-xs,
  .btn-sm {
    min-height: var(--tap-sm);
    padding: 10px 14px;
    font-size: var(--fs-sm);
  }
}

/* ═══ GRID ══════════════════════════════════════════════════════ */
/* minmax(0, 1fr) and not 1fr, on every one of these. A bare `1fr` is
   `minmax(auto, 1fr)`, and `auto` there is the track's min-content — so one
   child that cannot shrink drags the whole column past its share and the page
   with it. On Pilzsorten that child is a <select> whose longest option reads
   "— Rezept von anderer Sorte übernehmen —": a select is at least as wide as
   its widest option, 363px of it, inside a 343px grid. The page scrolled to
   443px at 375px wide. With the minimum pinned to 0 the track keeps its share
   and the select shrinks like everything else. */
.g2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
.g3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}
/* Die Anteile im Ansatz-Formular: vier oder fünf kurze Zahlenfelder. Keine
   feste Spaltenzahl, weil das Maismehl-Feld nur bei den Rezepten dabei ist, die
   welches vorsehen — und weil am Telefon zwei nebeneinander passen, am Tisch
   alle. */
.sb-blend-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: 8px 10px;
  margin-top: 4px;
}
.sb-blend-row label {
  color: var(--c-text-sec);
}
.g4 {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
}
/* Six, for the one place that needs six: the average-composition row in
   Inventory. It was an inline `repeat(6,1fr)` written straight into
   renderThresholds(), and a bare 1fr is minmax(auto, 1fr) — the track cannot
   go below its content's min-content width, and an <input> has a wide one. Six
   of them held a 285px container open to 486px, so the Inventory page scrolled
   sideways by 111px at 375px. Nothing could see it: the grid is a template
   string in app.js, so the source census reads it as text and measure-mobile.js
   serves the page with the scripts stripped. */
.g6 {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 12px;
}

/* ═══ METRIC CARDS ══════════════════════════════════════════════ */
.met {
  background: var(--c-surface);
  border-radius: var(--radius);
  padding: 16px 18px;
  border: 1px solid var(--c-border);
  box-shadow: var(--shadow);
  border-left: 4px solid var(--c-primary);
}
.met-l {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-sec);
  margin-bottom: 4px;
  font-weight: 500;
}
.met-v {
  font-size: 1.625rem;
  font-weight: 700;
  color: var(--c-text);
  line-height: 1.2;
}

/* ═══ BADGES ════════════════════════════════════════════════════ */
.badge {
  display: inline-block;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 99px;
}
/* Phase badges — the Status column of every table that lists a batch.

   These are the second half of a colour decision whose first half lives in the
   database. A zone's colour says which room a bag is in; this badge says which
   phase a batch is in; on this farm those are the same fact seen twice, so the
   two agreeing is not decoration, it is the point. They were both the
   full-saturation Tailwind 500 family, and both are now the muted version of
   the same three hues — see ZONE_SEED_COLOR in db.js.

   Backgrounds are the hue at 30% saturation and 94% lightness; the text is
   picked per hue to land on 6.0:1 against its own background, which is matched
   across the three so none of them reads as louder than its neighbours. Small
   text, so the floor is 4.5, not 3.

   The one thing this does NOT follow is the slate-to-green ramp used inside the
   Chargen tiles. That ramp encodes a sequence and works because the three
   phases sit side by side in one bar. A badge stands alone in its row, with no
   neighbour to compare against, which is the case the ramp is worst at — the
   same reason the zone colours kept their hues. */
.b-inc {
  background: #ebf1f4;
  color: #306077;
}
.b-tent {
  background: #ebf4f1;
  color: #2a6652;
}
.b-spawn {
  background: #f0ebf4;
  color: #7241a0;
}
.b-contam {
  background: #fee2e2;
  color: #991b1b;
}
.b-done {
  background: var(--c-bg);
  color: var(--c-text-muted);
}
.b-add {
  background: #dcfce7;
  color: #166534;
}
.b-move {
  background: #dbeafe;
  color: #1e40af;
}
.b-remove {
  background: #fee2e2;
  color: #991b1b;
}
.b-harvest {
  background: #fef3c7;
  color: #92400e;
}
.badge-mc {
  background: #f3e8ff;
  color: #6b21a8;
}
.badge-pd {
  background: #dbeafe;
  color: #1e40af;
}
.badge-lc {
  background: #dcfce7;
  color: #166534;
}
.badge-g2g {
  background: #fef3c7;
  color: #92400e;
}
.badge-sy {
  background: #cffafe;
  color: #155e75;
}
.badge-du {
  background: #f5ebe0;
  color: #7c4a1e;
}
.badge-active {
  background: #dcfce7;
  color: #166534;
}
.badge-stored {
  background: #dbeafe;
  color: #1e40af;
}
.badge-used {
  background: var(--c-bg);
  color: var(--c-text-muted);
}
.badge-sold {
  background: #ede9fe;
  color: #5b21b6;
}
.badge-contam {
  background: #fee2e2;
  color: #991b1b;
}

/* ═══ TABLES ════════════════════════════════════════════════════ */
table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8125rem;
}
th {
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-text-muted);
  text-align: left;
  padding: 8px 10px;
  border-bottom: 2px solid var(--c-border);
  position: sticky;
  top: 0;
  background: var(--c-surface);
  z-index: 1;
}
th.sortable {
  cursor: pointer;
  user-select: none;
}
@media (hover: hover) {
  th.sortable:hover {
    color: var(--c-text-sec);
  }
}
th.sortable .arrow {
  font-size: max(0.5625rem, var(--fs-min));
  opacity: 0.35;
  margin-left: 3px;
}
/* Touch devices have no hover, so workers can't tease out which columns are
   sortable. Bump the arrow opacity so all sort indicators are always visible. */
@media (pointer: coarse) {
  th.sortable .arrow {
    opacity: 0.7;
  }
}
th.sortable.active .arrow {
  opacity: 1;
  color: var(--c-primary);
}
td {
  padding: 10px;
  border-bottom: 1px solid var(--c-border-light);
  color: var(--c-text);
  vertical-align: middle;
}
tr:last-child td {
  border-bottom: none;
}
tr:nth-child(even) td {
  background: var(--c-bg);
}
tr:hover td {
  background: #f1f5f9;
}

/* ═══ MISC ══════════════════════════════════════════════════════ */
.empty {
  text-align: center;
  color: var(--c-text-muted);
  font-size: 0.8125rem;
  padding: 3rem;
}
.sp-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  margin-right: 5px;
  vertical-align: middle;
  /* Species colours track the real mushroom, so some land near-white (Lions
     Mane) and some near-black (Black Pearl King). An inset ring keeps both
     readable on the light card and on a dark surface. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.22);
}
.cal-event .sp-dot {
  width: 8px;
  height: 8px;
  border: 1.5px solid rgba(255, 255, 255, 0.8);
  margin-right: 4px;
}
/* No `display` here on purpose — see the .t-cards .sub-tag rule in the phone
   block. As a bare inline box this pill splits across line boxes when it
   wraps, which is a real blemish on the desktop table too, but the desktop
   Substrat column is ~63px wide: making the pill a box there wraps it into
   three lines of padded box and takes #t-batches from 760px to 1093px tall.
   That is a desktop layout change, not a phone fix, and it needs the column
   widths looked at rather than a display keyword. */
.sub-tag {
  font-size: max(0.6875rem, var(--fs-min));
  background: var(--c-bg);
  color: var(--c-text-sec);
  padding: 3px 8px;
  border-radius: 99px;
  margin-right: 3px;
}
.harvest-bar {
  height: 5px;
  border-radius: 3px;
  background: var(--c-border);
  overflow: hidden;
  margin-top: 4px;
}
.harvest-bar-fill {
  height: 100%;
  border-radius: 3px;
  background: #f59e0b;
}
/* ── Dashboard week grid ──────────────────────────────────────────────────
   Seven days across, each column carrying its own work, so the whole week is
   readable without opening anything. The day headers and the day bodies are
   both direct grid children rather than the body sitting inside its header's
   column — that is what lets a narrow screen collapse to one open day spanning
   the full width, from the same markup. */
.wk {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 4px;
  margin-bottom: 8px;
}
/* The column comes in as --col rather than an inline grid-column, because an
   inline declaration outranks any stylesheet rule — including the media query
   below, which then silently failed to widen the open day and left it in a
   43px column on a phone. */
.wk-h {
  grid-row: 1;
  grid-column: var(--col);
  min-width: 0;
  padding: 4px 1px 5px;
  border: 1px solid transparent;
  border-bottom: 2px solid var(--c-border);
  border-radius: 6px 6px 0 0;
  background: transparent;
  cursor: pointer;
  font-family: inherit;
  line-height: 1.15;
  overflow: hidden;
}
.wk-h.sel {
  border-bottom-color: var(--c-accent);
  background: var(--c-bg);
}
.wk-b {
  grid-row: 2;
  grid-column: var(--col);
  min-width: 0;
  font-size: max(0.6875rem, var(--fs-min));
  /* A rule between the days, not just a gap. Seven columns of similar-looking
     rows run together without one, and the eye loses which day it is reading.
     Left edge on every column but the first, so the outer edges stay clean. */
  border-left: 1px solid var(--c-border);
  padding: 4px 5px 0;
}
.wk-b:first-of-type {
  border-left: 0;
}
.wk-h {
  border-right: 1px solid transparent;
}
/* Ein Punkt im Spaltenkopf, wenn an diesem Tag eine eigene Aufgabe fällt.

   Unter 769px ist der Spaltenkörper ausgeblendet, also sieht man von sechs der
   sieben Tage nur den Kopf — und die benannte Aufgabe mit ihrem Haken steckt im
   Körper. Die Zahl in der Fußzeile bewegte sich zwar, sagte aber nicht, dass
   etwas Eigenes dahintersteckt. Ein Punkt passt in 43px und tut es. */
.wk-h-own {
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-left: 3px;
  border-radius: 50%;
  background: var(--c-purple-dark);
  vertical-align: middle;
}
/* The selected day is the one you are working from, so it gets a tinted
   channel down the whole column rather than only a marked header. */
.wk-b.sel {
  background: var(--c-bg);
}
/* Der Arbeitszettel des gewählten Tages, über die ganze Breite.

   Er stand in der Spalte des Tages, und eine Spalte ist ein Siebtel der Karte.
   Auf dem Telefon fiel das nicht auf, weil dort nur diese eine Spalte sichtbar
   ist und über alle sieben geht; am Rechner wurde derselbe Zettel in 170px
   gepresst — „Black..." statt der Sorte, „0,0 von 10,0" über drei Zeilen, und
   ein Knopf breiter als seine Spalte.

   Oben die Woche auf einen Blick, darunter der Tag, an dem gearbeitet wird. An
   jedem Schirm dieselbe Anordnung, statt zweier Layouts, von denen eines nie
   angesehen wurde. */
.wk-open {
  grid-row: 3;
  grid-column: 1 / -1;
  min-width: 0;
  margin-top: 6px;
  padding-top: 8px;
  border-top: 1px solid var(--c-border);
}

/* Die Zählung einer Spalte — ein Knopf, der den Tag öffnet.

   Vorher war sie totes Papier: „4 Achtung, 4 Ansetzen, 5 überfällig" stand da,
   man tippte darauf, und nichts geschah. Ein Kasten mit Zahlen darin sieht aus
   wie etwas zum Aufklappen, also klappt er jetzt auf.

   Randlos und ohne Fläche, wie die Mengenzahl darüber: eine antippbare Fläche,
   kein Knopf. Ein Knopf um alle Zeilen und nicht einer je Zeile — sie führen
   alle an dieselbe Stelle. */
.wk-counts {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: inherit;
  text-align: left;
  cursor: pointer;
  border-radius: 4px;
}
.wk-counts:hover {
  background: var(--c-bg);
}
/* Der Tag, der schon offen ist: dieselbe Zählung, aber nichts zu drücken. Ein
   Zeiger und ein Aufleuchten unter der Maus versprechen sonst etwas, das
   bereits geschehen ist. */
.wk-counts.is-open {
  cursor: default;
}
.wk-counts.is-open:hover {
  background: none;
}
.wk-count {
  display: flex;
  align-items: baseline;
  gap: 5px;
  padding: 2px;
  margin-bottom: 2px;
  border-left: 2px solid var(--c-text-muted);
}
.wk-count-n {
  font-weight: 700;
  flex-shrink: 0;
}
.wk-count-t {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--c-text-sec);
}
/* Versäumtes ist das Einzige, was den Blick quer über die Woche ziehen soll. */
.wk-count-late {
  display: block;
  padding: 1px 2px;
  color: var(--c-red-dark);
  font-size: var(--fs-micro);
}

/* Woche oder Tag. Sieben Spalten sind eine Übersicht und ein Kompromiss: jede
   ist ein Siebtel breit, und was darin steht, muss klein sein. Wer den Tag
   arbeitet, braucht die Übersicht gerade nicht — dann fällt sie weg und der Tag
   bekommt die ganze Karte samt größerer Schrift. */
/* Kein gap: die beiden Knöpfe stoßen aneinander, und der rechte spart sich
   seinen linken Rand. Mit 2px Abstand DAZWISCHEN fehlte dem rechten Knopf eine
   Kante — er las sich als offener Kasten, nicht als zweites Segment. */
.wk-view {
  display: flex;
  margin-bottom: 8px;
}
.wk-view-btn {
  min-height: var(--tap-sm);
  padding: 4px 12px;
  border: 1px solid var(--c-border);
  background: var(--c-surface);
  color: var(--c-text-sec);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
}
.wk-view-btn:first-child {
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}
.wk-view-btn:last-child {
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  border-left-width: 0;
}
.wk-view-btn.sel {
  background: var(--c-accent);
  border-color: var(--c-accent);
  color: #fff;
}
/* Ohne den Streifen braucht der Tag einen anderen Weg zum nächsten. */
.wk.day-only {
  display: block;
}
.wk.day-only .wk-open {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}
.wk-daynav {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.wk-daynav-t {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 700;
  text-align: center;
}
.wk-daynav-btn {
  flex: 0 0 auto;
  justify-content: center;
  padding: 4px 12px;
}
/* Am ersten und am letzten Tag ist ein Pfeil aus. .btn setzt Farbe und Fläche
   als Autorenregeln und schlägt damit das Ausgrauen des Browsers, also sah der
   tote Pfeil aus wie der lebende daneben — wieder ein Feld, das gedrückt wird
   und nichts tut. Es gab im ganzen Blatt genau eine :disabled-Regel. */
.wk-daynav-btn:disabled {
  opacity: 0.35;
  cursor: default;
}
/* Der ganze Sinn der Tagesansicht: der Platz, den die sechs anderen Spalten
   nicht mehr brauchen, geht in die Schrift. Zwei Klassen tief, weil .fs-sm sich
   selbst verdoppelt (.fs-sm.fs-sm) und sonst gewänne. */
.wk.day-only .wk-open .fs-sm.fs-sm {
  font-size: var(--fs-base);
}
.wk.day-only .wk-open .fs-xs.fs-xs {
  font-size: var(--fs-sm);
}
/* Unter dieser Breite sind die sieben Spalten zu schmal für irgendeinen Inhalt,
   also bleibt vom Raster der Kopfstreifen — der Tag darunter steht ohnehin
   schon über alle sieben. 768px ist die Marke, die das Dashboard sonst auch
   benutzt. */
@media (max-width: 768px) {
  .wk-b {
    display: none;
  }
}
/* Die Vorgabezahl eines Tages, im Kopf. Sie steht auch im Spaltenkörper, und
   dort ist sie bearbeitbar — also gehört sie oben nur dorthin, wo der Körper
   nicht zu sehen ist. Über 768px sind alle sieben Körper offen, und dieselbe
   Zahl zweimal übereinander war genau das, was die Woche doppelt aussehen
   liess; darunter zeigen sechs von sieben Tagen nur ihren Kopf, und ohne sie
   waere der Wochenüberblick auf dem Telefon eine Reihe blosser Themen. */
@media (min-width: 769px) {
  .wk-h-t {
    display: none;
  }
}
.todo-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  margin-bottom: 8px;
  border: 1px solid var(--c-border);
  /* Der Streifen beantwortet genau eine Frage: wie dringend ist diese Zeile.
     Vorher stand hier die Sortenfarbe, während Rahmen und Hintergrund derselben
     Zeile den Zustand färbten — Rot hieß dann „Reishi" oder „dringend", je
     nachdem, worauf man sah. Die Sorte steht jetzt als Punkt neben ihrem Namen,
     wo sie nie gestört hat. --action-color bleibt die Übersteuerung des
     Kalenders. */
  border-left: 4px solid var(--action-color, var(--c-border));
  background: var(--c-surface);
  transition: box-shadow 0.15s;
}
@media (hover: hover) {
  .todo-row:hover {
    box-shadow: var(--shadow);
  }
}
.todo-row.urgent {
  border-color: #fecaca;
  border-left-color: var(--c-red);
  background: #fff5f5;
}
.todo-row.warn {
  border-color: #fde68a;
  border-left-color: var(--c-amber);
  background: #fffbeb;
}
.todo-row input[type='checkbox'] {
  width: 16px;
  height: 16px;
  margin-top: 2px;
  flex-shrink: 0;
  cursor: pointer;
  accent-color: var(--c-primary);
}
/* Bigger checkboxes on touch — 16px is unreachable for gloved fingers and
   even bare-handed it's a precision tap. The .todo-row already has 12px
   padding so the combined hit area lands above the 44px minimum once the
   checkbox itself bumps to 24px. */
@media (pointer: coarse) {
  .todo-row input[type='checkbox'] {
    width: 24px;
    height: 24px;
    margin-top: 0;
  }
}
/* Priority indicator dot for dashboard tasks. Was a 9 px color-only chip
   (red/amber/green) with no text fallback — fails for color-blind users
   per audit Section 3.4. Bumped to 14 px with a letter inside (H/M/L)
   so the priority is legible without relying on color. */
.bc-cell {
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  padding: 8px;
  text-align: center;
  background: var(--c-surface);
}
.tag {
  display: inline-block;
  font-size: max(0.6875rem, var(--fs-min));
  padding: 2px 8px;
  border-radius: 99px;
  margin-left: 4px;
  vertical-align: middle;
}
.caldav-status {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-left: 6px;
  vertical-align: middle;
}
.caldav-status.synced {
  background: var(--c-green);
}
.caldav-status.pending {
  background: var(--c-amber);
}
.caldav-status.error {
  background: var(--c-red);
}

/* Sync dots */
.sync-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #ddd;
  cursor: pointer;
  flex-shrink: 0;
}
.sync-dot.ok {
  background: var(--c-green);
}
.sync-dot.err {
  background: var(--c-red);
}
.sync-dot.busy {
  background: var(--c-amber);
}

/* ═══ CALENDAR ══════════════════════════════════════════════════ */
.cal-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
  flex-wrap: wrap;
  gap: 10px;
}
.cal-toolbar-left {
  display: flex;
  gap: 6px;
  align-items: center;
}
.cal-toolbar-right {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}
.cal-nav-btn {
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text);
  cursor: pointer;
  font-size: 1rem;
  transition: all 0.15s;
}
.cal-nav-btn:hover {
  background: var(--c-primary-light);
  border-color: var(--c-primary);
  color: var(--c-primary);
}
.cal-today-btn {
  height: 36px;
  padding: 0 16px;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text);
  cursor: pointer;
  font-size: 0.8125rem;
  font-weight: 600;
  font-family: inherit;
  transition: all 0.15s;
}
.cal-today-btn:hover {
  background: var(--c-primary);
  color: #fff;
  border-color: var(--c-primary);
}
.cal-title {
  font-size: 1.25rem;
  font-weight: 800;
  color: var(--c-text);
  margin-left: 8px;
  letter-spacing: -0.3px;
}
.cal-view-toggle {
  display: flex;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}
.cal-vbtn {
  padding: 7px 18px;
  font-size: 0.8125rem;
  font-weight: 600;
  border: none;
  background: var(--c-surface);
  color: var(--c-text-sec);
  cursor: pointer;
  font-family: inherit;
  transition: all 0.15s;
  white-space: nowrap;
}
.cal-vbtn:first-child {
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}
.cal-vbtn:last-child {
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.cal-vbtn.active {
  background: var(--c-primary);
  color: #fff;
}
.cal-vbtn:hover:not(.active) {
  background: var(--c-primary-light);
  color: var(--c-primary);
}
.cal-print-btn {
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text-sec);
  cursor: pointer;
  font-size: 1rem;
  transition: all 0.15s;
  flex-shrink: 0;
}
.cal-print-btn:hover {
  background: var(--c-primary-light);
  border-color: var(--c-primary);
  color: var(--c-primary);
}
.cal-add-btn {
  height: 36px;
  padding: 0 16px;
  border: none;
  border-radius: var(--radius-sm);
  background: var(--c-primary);
  color: #fff;
  cursor: pointer;
  font-size: 0.8125rem;
  font-weight: 600;
  font-family: inherit;
  transition: all 0.15s;
  display: flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
  flex-shrink: 0;
}
.cal-add-btn:hover {
  background: var(--c-primary-hover);
}
.cal-legend {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 14px;
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-sec);
  padding: 8px 12px;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
}
.cal-legend span {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 500;
}
.cal-legend span::before {
  content: '';
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 3px;
  flex-shrink: 0;
}
.cal-legend .leg-batch::before {
  background: #ef4444;
}
.cal-legend .leg-task::before {
  background: #3b82f6;
}
.cal-legend .leg-harvest::before {
  background: #f59e0b;
}
.cal-legend .leg-custom::before {
  background: #16a34a;
}
.cal-legend .leg-meeting::before {
  background: #8b5cf6;
}
.cal-legend .leg-delivery::before {
  background: #14b8a6;
}
.cal-legend .leg-maintenance::before {
  background: #64748b;
}
.cal-legend .leg-import::before {
  background: #6366f1;
}

/* Month grid */
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px;
  background: var(--c-border);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  overflow: hidden;
}
.cal-hdr {
  background: var(--c-bg);
  text-align: center;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 700;
  color: var(--c-text-muted);
  padding: 10px 4px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.cal-cell {
  background: var(--c-surface);
  min-height: 110px;
  padding: 6px 8px;
  position: relative;
  transition: background 0.15s;
  cursor: pointer;
}
.cal-cell:hover {
  background: #fafffe;
}
.cal-cell.other {
  background: var(--c-bg);
}
.cal-cell.other .cal-day {
  color: #cbd5e1;
}
.cal-cell.other:hover {
  background: #f1f5f9;
}
.cal-cell.today {
  background: var(--c-primary-light);
}
.cal-day {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--c-text-sec);
  margin-bottom: 5px;
  /* Der Kreis öffnet den Tag, ist also ein Ziel und kein Datum. Mit 28 px war
     er unter jeder Breite ab 769 px zu klein für einen Finger, und ein
     Monatsraster wird stehend an der Wand bedient. `--tap-sm-min` und nicht
     `--tap-min`: 48 statt 56, weil sieben Spalten davon bei 769 px noch reichlich
     Platz haben (109 px je Spalte) und der Kreis nicht die Zelle werden soll.
     Am Rechner bleibt es bei 28, weil `max()` dort gegen 24 gewinnt. */
  width: max(28px, var(--tap-sm-min));
  height: max(28px, var(--tap-sm-min));
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  transition: all 0.15s;
}
.cal-cell.today .cal-day {
  background: var(--c-primary);
  color: #fff;
  font-weight: 700;
}
.cal-day:hover {
  background: var(--c-primary-light);
  color: var(--c-primary);
}
.cal-cell.today .cal-day:hover {
  background: var(--c-primary-hover);
  color: #fff;
}
.cal-event {
  font-size: max(0.6875rem, var(--fs-min));
  color: #fff;
  padding: 2px 7px;
  border-radius: 4px;
  margin-bottom: 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: grab;
  line-height: 1.5;
  font-weight: 500;
  transition:
    opacity 0.15s,
    transform 0.15s;
}
/* Touch devices: HTML5 drag-and-drop doesn't work on iOS Safari without a
   pointer-event polyfill, and `cursor:grab` triggers iOS text-selection on
   long-press, which competes with the tap-to-open. Make the event read as
   tappable on touch — drag is desktop-only. */
@media (pointer: coarse) {
  .cal-event {
    cursor: pointer;
  }
}
@media (hover: hover) {
  .cal-event:hover {
    opacity: 0.9;
    transform: translateY(-1px);
  }
}
.cal-event.no-drag {
  cursor: default;
  opacity: 0.8;
}
@media (hover: hover) {
  .cal-event.no-drag:hover {
    transform: none;
    opacity: 0.85;
  }
}
.cal-ev-assignees {
  font-size: max(0.625rem, var(--fs-min));
  opacity: 0.85;
  display: inline;
  white-space: nowrap;
}
.assignee-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: max(0.6875rem, var(--fs-min));
  padding: 1px 8px;
  background: #eee;
  border-radius: 12px;
}
.assignee-chip button {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 0.8125rem;
  padding: 0;
  line-height: 1;
}
#cal-ev-assignee-dropdown label {
  display: block;
  padding: 4px 8px;
  cursor: pointer;
  font-size: max(0.75rem, var(--fs-min));
}
#cal-ev-assignee-dropdown label:hover {
  background: #f0f0f0;
}
.cal-more {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-primary);
  cursor: pointer;
  padding: 2px 7px;
  font-weight: 600;
  border-radius: 4px;
  transition: background 0.15s;
}
.cal-more:hover {
  background: var(--c-primary-light);
}
.cal-cell.drag-over {
  background: #dcfce7;
  box-shadow: inset 0 0 0 2px var(--c-primary);
}

/* Week view */
.cal-week {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--c-surface);
}
.cal-week-hdr {
  display: grid;
  grid-template-columns: 56px repeat(7, minmax(0, 1fr));
  background: var(--c-bg);
  border-bottom: 1px solid var(--c-border);
}
.cal-week-hdr-cell {
  text-align: center;
  padding: 10px 4px;
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 700;
  color: var(--c-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.3px;
}
.cal-week-hdr-cell .wk-day-num {
  display: block;
  font-size: 1.25rem;
  font-weight: 800;
  color: var(--c-text);
  letter-spacing: -0.5px;
  margin-top: 2px;
}
.cal-week-hdr-cell.today-col {
  color: var(--c-primary);
  background: var(--c-primary-light);
}
.cal-week-hdr-cell.today-col .wk-day-num {
  color: var(--c-primary);
}
.cal-week-allday {
  display: grid;
  grid-template-columns: 56px repeat(7, minmax(0, 1fr));
  border-bottom: 1px solid var(--c-border);
  min-height: 36px;
}
.cal-week-allday-label {
  font-size: max(0.625rem, var(--fs-min));
  color: var(--c-text-muted);
  padding: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--c-bg);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.3px;
}
.cal-week-allday-cell {
  padding: 4px 5px;
  border-left: 1px solid var(--c-border-light);
  background: var(--c-surface);
  min-height: 36px;
}
.cal-week-body {
  display: grid;
  grid-template-columns: 56px repeat(7, minmax(0, 1fr));
  position: relative;
  max-height: 600px;
  overflow-y: auto;
}
.cal-week-time {
  font-size: max(0.625rem, var(--fs-min));
  color: var(--c-text-muted);
  text-align: right;
  padding: 0 8px 0 0;
  height: 48px;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  background: var(--c-bg);
  border-top: 1px solid var(--c-border-light);
  font-weight: 500;
}
.cal-week-slot {
  border-left: 1px solid var(--c-border-light);
  border-top: 1px solid var(--c-border-light);
  height: 48px;
  position: relative;
  background: var(--c-surface);
  cursor: pointer;
  transition: background 0.1s;
}
.cal-week-slot:hover {
  background: var(--c-primary-light);
}
.cal-week-slot.today-col {
  background: #fafffc;
}
.cal-week-ev {
  position: absolute;
  left: 2px;
  right: 2px;
  border-radius: 5px;
  padding: 2px 6px;
  font-size: max(0.6875rem, var(--fs-min));
  color: #fff;
  overflow: hidden;
  z-index: 2;
  cursor: grab;
  font-weight: 500;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
  transition: opacity 0.15s;
  line-height: 1.3;
}
.cal-week-ev:hover {
  opacity: 0.9;
}
.cal-week-ev .ev-resize {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 6px;
  cursor: ns-resize;
  border-radius: 0 0 5px 5px;
}
.cal-week-ev .ev-resize:hover {
  background: rgba(255, 255, 255, 0.3);
}
.cal-week-now-line {
  position: absolute;
  left: 56px;
  right: 0;
  height: 2px;
  background: var(--c-primary);
  z-index: 5;
  pointer-events: none;
}
.cal-week-now-line::before {
  content: '';
  position: absolute;
  left: -4px;
  top: -3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--c-primary);
}

/* Day view */
.cal-day-view {
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--c-surface);
}
.cal-day-allday {
  padding: 10px 14px;
  background: var(--c-bg);
  border-bottom: 1px solid var(--c-border);
  min-height: 44px;
}
.cal-day-allday .sec {
  margin-bottom: 6px;
  font-size: max(0.6875rem, var(--fs-min));
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--c-text-muted);
  font-weight: 700;
}
.cal-day-allday-empty {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
  font-style: italic;
}
.cal-day-body {
  display: grid;
  grid-template-columns: 56px 1fr;
  max-height: 600px;
  overflow-y: auto;
  position: relative;
}
.cal-day-time {
  font-size: max(0.625rem, var(--fs-min));
  color: var(--c-text-muted);
  text-align: right;
  padding: 0 8px;
  height: 48px;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  background: var(--c-bg);
  border-top: 1px solid var(--c-border-light);
  font-weight: 500;
}
.cal-day-slot {
  border-top: 1px solid var(--c-border-light);
  height: 48px;
  position: relative;
  background: var(--c-surface);
  cursor: pointer;
  transition: background 0.1s;
}
.cal-day-slot:hover {
  background: var(--c-primary-light);
}
.cal-day-ev {
  position: absolute;
  left: 4px;
  right: 4px;
  border-radius: 5px;
  padding: 3px 8px;
  font-size: max(0.75rem, var(--fs-min));
  color: #fff;
  overflow: hidden;
  z-index: 2;
  cursor: grab;
  font-weight: 500;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
  transition: opacity 0.15s;
  line-height: 1.3;
}
.cal-day-ev:hover {
  opacity: 0.9;
}
.cal-day-ev .ev-resize {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 6px;
  cursor: ns-resize;
  border-radius: 0 0 5px 5px;
}
.cal-day-ev .ev-resize:hover {
  background: rgba(255, 255, 255, 0.3);
}
.cal-day-now-line {
  position: absolute;
  left: 56px;
  right: 0;
  height: 2px;
  background: var(--c-primary);
  z-index: 5;
  pointer-events: none;
}
.cal-day-now-line::before {
  content: '';
  position: absolute;
  left: -4px;
  top: -3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--c-primary);
}

/* Drag/resize active states */
.cal-week-ev.dragging,
.cal-day-ev.dragging {
  opacity: 0.7;
  z-index: 10;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}

/* Eight columns of numbers that mean nothing squashed. The width was inline on
   the table, which is why it survived into the card layout and forced a 640px
   scroll on a phone that no longer has a table to scroll. Stated where it is
   true instead: above the breakpoint, where this is still a table. */
@media (min-width: 769px) {
  #t-harvestrelease {
    min-width: 640px;
  }
}

/* ═══ CALENDAR — AGENDA VIEW ═══════════════════════════════════
   The calendar below 769px. Not a fourth tab: a month cell at 375px is 47.9px
   wide and gives an event label 29.9px of text, four glyphs and an ellipsis,
   and week view is worse at 40.7px per column. Seven columns cannot be made to
   work by making rows taller — the constraint is horizontal.

   A list has the opposite shape: one column, full width, as many rows as the
   month has entries. Everything the grid had to abbreviate fits here in words. */
.cal-agenda-day {
  display: flex;
  gap: 12px;
  padding: 10px 0;
  border-top: 1px solid var(--c-border);
}
.cal-agenda-day:first-child {
  border-top: 0;
}
/* The date column is fixed so every row's text starts on the same line, which
   is what makes a list scannable rather than merely short. */
.cal-agenda-date {
  flex: 0 0 40px;
  text-align: center;
  padding-top: 2px;
}
.cal-agenda-num {
  display: block;
  font-size: var(--fs-base);
  font-weight: 700;
  line-height: 1.1;
}
.cal-agenda-dow {
  display: block;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
.cal-agenda-day.today .cal-agenda-num {
  color: var(--c-primary);
}
.cal-agenda-rows {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* A button, not a div: this opens the same editor the month grid opens, and
   the row is the target — 56px of it, the Feld floor, because the calendar is
   read walking between tents. */
.cal-agenda-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  width: 100%;
  min-height: var(--tap-min);
  padding: 8px 10px;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text);
  font-family: inherit;
  font-size: var(--fs-xs);
  text-align: left;
  cursor: pointer;
}
.cal-agenda-dot {
  flex: 0 0 10px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  margin-top: 4px;
}
.cal-agenda-when {
  flex: 0 0 auto;
  color: var(--c-text-sec);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.cal-agenda-what {
  flex: 1;
  min-width: 0;
}
.cal-agenda-label {
  display: block;
  font-weight: 600;
}
.cal-agenda-meta,
.cal-agenda-desc {
  display: block;
  color: var(--c-text-muted);
}
.cal-agenda-who::before {
  content: ' · ';
}
.cal-agenda-empty {
  padding: 24px 0;
  text-align: center;
  color: var(--c-text-muted);
  font-style: italic;
}

/* ═══ DASHBOARD LOCATION OVERVIEW ══════════════════════════════ */
.location-section {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: 20px;
  margin-bottom: 14px;
  box-shadow: var(--shadow);
}
.location-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
  cursor: pointer;
  user-select: none;
  /* Sie klappt eine ganze Ortsgruppe auf und zu, ist also ein Bedienelement
     und kein Titel — mit 21 px war sie auf beiden Achsen unter der Marke, mit
     der Maus unter 24 und mit dem Finger unter 44. Die Marke kommt aus dem
     Token, damit sie der Eingabeachse folgt und nicht einer Breite. */
  min-height: var(--tap-sm);
}
.location-section.collapsed .location-section-header {
  margin-bottom: 0;
}
.location-section-body {
  overflow: hidden;
  transition: max-height 0.25s ease;
}
.location-section.collapsed .location-section-body {
  max-height: 0 !important;
  margin-top: 0;
}
.location-section-toggle {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  transition: transform 0.2s ease;
  color: var(--c-text-muted);
}
.location-section.collapsed .location-section-toggle {
  transform: rotate(-90deg);
}
.location-section-title {
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--c-text);
  display: flex;
  align-items: center;
  gap: 8px;
}
.location-section-title .zone-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  flex-shrink: 0;
}
.location-section-count {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
  font-weight: 500;
}
.rack-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}
.rack-grid.rack-grid-5col {
  grid-template-columns: repeat(5, 1fr);
}
.rack-card-new {
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: 14px;
  background: var(--c-surface);
  transition: all 0.15s;
}
@media (hover: hover) {
  .rack-card-new:hover {
    background: var(--c-bg);
    box-shadow: var(--shadow);
  }
}
.rack-card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 6px;
}
.rack-card-name {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--c-text);
}
.rack-card-count {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
  font-weight: 500;
}
.rack-card-bar {
  height: 4px;
  border-radius: 2px;
  background: var(--c-border-light);
  overflow: hidden;
  margin-bottom: 8px;
}
.rack-card-bar-fill {
  height: 100%;
  border-radius: 2px;
  transition: width 0.3s ease;
}
.rack-card-batches {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.batch-card {
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--c-bg);
  border: 1px solid var(--c-border-light);
  border-left: 3px solid var(--c-border-light);
  cursor: pointer;
  transition: all 0.15s;
}
@media (hover: hover) {
  .batch-card:hover {
    border-color: var(--c-border);
    box-shadow: var(--shadow);
  }
}
.batch-card.batch-overdue {
  border-color: #fecaca;
  border-left-color: var(--c-red);
  background: #fff5f5;
}
.batch-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: max(0.75rem, var(--fs-min));
}
.batch-card-species {
  font-weight: 600;
  color: var(--c-text);
  display: flex;
  align-items: center;
  gap: 4px;
}
.batch-card-count {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
  font-weight: 500;
}
.batch-card-meta {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
  margin-top: 3px;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}
.batch-card-meta .overdue-text {
  color: #b91c1c;
  font-weight: 600;
}
.batch-card-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 6px;
  display: none;
}
.batch-card.expanded .batch-card-chips {
  display: flex;
}
.bag-chip {
  font-size: max(0.6875rem, var(--fs-min));
  font-family: ui-monospace, 'Cascadia Code', monospace;
  padding: 4px 8px;
  border-radius: 6px;
  cursor: pointer;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  color: var(--c-text);
  transition: all 0.1s;
  user-select: none;
}
.bag-chip.selected {
  background: #1a1a1a;
  color: #fff;
  border-color: #1a1a1a;
}
.tent-columns {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}
.tent-column {
  min-height: 80px;
}
.tent-column-header {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--c-text);
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.tent-column-empty {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
  font-style: italic;
  text-align: center;
  padding: 20px 10px;
  border: 1px dashed var(--c-border);
  border-radius: var(--radius-sm);
}
.contam-section {
  border-color: #fecaca;
  background: #fff5f5;
}
.contam-section .location-section-title {
  color: #b91c1c;
}
.action-bar-sticky {
  position: sticky;
  bottom: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: 10px 14px;
  margin-top: 10px;
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
}
.action-bar-sticky .action-bar-count {
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 600;
  color: var(--c-text);
}
/* ═══ OVERVIEW PERIOD TOGGLE ════════════════════════════════════ */
/* The period pills and the CSV button. Wrapping is not decoration: measured at
   375px the row is 382px wide, and it was invisible to scripts/measure-mobile.js
   for as long as the section around it carried an inline display:none. */
.ov-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}
.ov-period-toggle {
  display: flex;
  /* Drei Pillen, die nicht umbrechen konnten, und auf Deutsch sind sie
     zusammen 344 px breit: „Diese Woche | Diesen Monat | Dieses Jahr". Auf
     einem 320-px-Telefon war das der einzige Grund, warum die Übersicht als
     Ganzes breiter war als der Schirm — mit dem Finger verkleinerte das
     Telefon die ganze Seite auf 89 Prozent, mit der Maus lief sie 17 bis 40 px
     über den Rand. Ein Fehler mit zwei Gesichtern, wie im Labor.

     `flex-wrap` und `max-width` statt einer Umbruchstelle: Es gibt keine
     Breite, ab der das richtig wird, es gibt nur „passt" und „passt nicht",
     und genau das rechnet der Umbruch selbst aus. Auf Englisch passt dieselbe
     Zeile auch bei 320 px in eine Reihe und bleibt dort. */
  flex-wrap: wrap;
  background: var(--c-border-light);
  border-radius: 10px;
  padding: 3px;
  gap: 2px;
  width: fit-content;
  max-width: 100%;
  margin-bottom: 14px;
}
.ov-period-btn {
  padding: 6px 16px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
}
.ov-period-btn.active {
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}

/* ═══ LOCATIONS HEADER + INLINE SEARCH ═════════════════════════ */
.dash-locations-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.dash-search-inline {
  width: 200px;
  font-size: max(0.75rem, var(--fs-min));
  padding: 6px 10px;
  border: 2px solid var(--c-border-strong, #94a3b8);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text);
  font-family: inherit;
  transition: border-color 0.15s;
}
.dash-search-inline:focus {
  outline: none;
  border-color: var(--c-primary);
  box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.1);
}

/* ═══ SUPPLY LIGHT ═══════════════════════════════════════════════
   Under a Sorte's name on the Chargen list (Sortenliste): whether its supply
   needs acting on. */
/* The supply light. A dot and a word, not a filled pill — only the state that
   needs acting on today carries weight, so a card of healthy Sorten reads as
   plain text. */
.sup {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  color: var(--c-text-sec);
  white-space: nowrap;
}
.sup::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--sup-c, var(--c-slate));
  flex: none;
}
.sup-now {
  color: #7f1d1d;
  font-weight: 700;
  --sup-c: #b91c1c;
}
.sup-nospawn {
  --sup-c: #c2410c;
}
.sup-low {
  --sup-c: #a16207;
}
.sup-ok {
  color: var(--c-text-muted);
  --sup-c: #4d9c6a;
}
/* Out of the season's programme. Deliberately NOT green: green would claim the
   supply is fine, when the truth is that nobody is asking. A hollow dot says
   "no opinion", which is the honest one. */
.sup-off {
  color: var(--c-text-muted);
  --sup-c: transparent;
}
.sup-off::before {
  box-shadow: inset 0 0 0 1.5px var(--c-text-muted);
}
/* ═══ LAB STOCK TILES ════════════════════════════════════════════
   The Labor card reuses the Chargen tile wholesale — same chrome, same bar,
   same supply light — because it answers the same shape of question about a
   different shelf. Only three things are its own: a type chip, the per-Sorte
   rows, and the fact that the tile is not a button. */
.stile-static {
  cursor: default;
}
@media (hover: hover) {
  .stile-static:hover {
    border-color: var(--c-border);
    box-shadow: none;
  }
  .stile-static.urgent:hover {
    border-color: var(--c-red-strong);
    box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
  }
}
.lab-chip {
  font-size: max(0.625rem, var(--fs-min));
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 5px;
  margin-right: 6px;
  flex: none;
}
/* Eine Woche je Zeile: Nummer, Zeitraum, Balken, Zahl gegen Ziel, Ampel.
   Dieselben Bausteine wie die Sorten- und Labor-Kacheln — .sbar für den Balken,
   .sup für die Ampel — damit "geschafft" hier aussieht wie "Bestand reicht"
   dort und nicht wie eine neue Sprache. */
.lab-rows {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.lab-row {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-sec);
  min-width: 0;
}
/* Eine Bestandszeile, die ihre eigene Untergrenze setzt. Sie muss ein Knopf
   sein, damit sie mit der Tastatur erreichbar ist — und dann muss ihr alles
   genommen werden, was ein Knopf mitbringt, sonst sitzt sie anders im Raster
   als die Zeilen ohne Pilzsorten-Eintrag daneben. */
.lab-row-btn {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  width: 100%;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  border-radius: 4px;
}
.lab-row-btn:hover {
  background: var(--c-border-light);
}
.lab-row-btn:focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 1px;
}
.lab-row-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
  border: 1px solid rgba(0, 0, 0, 0.12);
}
.lab-row-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lab-row-n {
  font-weight: 700;
  color: var(--c-text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Laborbestand je Sorte (renderLabStock, renderNachproduzieren, lbSheetRender).
   One row per species with the same five fields in the same places; amber under
   its target, red for an order that cannot ship. */
.vb-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  margin-top: 8px;
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
}
.vb-bb {
  margin-top: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--c-primary-light);
  color: var(--c-text-sec);
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.4;
}
.vb-bb[hidden] {
  display: none;
}
.vb-bb.vb-bb-warn {
  background: var(--c-amber-light);
  color: var(--c-amber-dark);
}

/* A species: a button the width of the card, because the whole row opens it. */
.lb-sp {
  display: block;
  width: 100%;
  padding: 10px 0;
  border: 0;
  border-bottom: 1px solid var(--c-border-light);
  background: transparent;
  color: inherit;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.lb-sp:last-child {
  border-bottom: 0;
}
.lb-sp:focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 2px;
}
.lb-sp-h {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 6px;
}
.lb-sp-h b {
  flex: 0 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}
.lb-paused {
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
}
.lb-chev {
  margin-left: auto;
  color: var(--c-text-muted);
  font-size: 1.25rem;
  line-height: 1;
}
.lb-row {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 4px;
}
.lb-st {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  padding: 4px 2px;
  border-radius: 8px;
  background: var(--c-bg);
  font-variant-numeric: tabular-nums;
}
.lb-st small {
  color: var(--c-text-muted);
  font-size: max(0.6875rem, var(--fs-min));
  white-space: nowrap;
}
.lb-st b {
  font-size: 0.9375rem;
  white-space: nowrap;
}
.lb-st em {
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
  font-style: normal;
  font-weight: 500;
}
.lb-st.lb-zero b {
  color: var(--c-text-muted);
  font-weight: 500;
}
.lb-st.lb-under {
  background: var(--c-amber-light);
}
.lb-st.lb-under b {
  color: var(--c-amber-dark);
}
.lb-st.lb-now {
  background: var(--c-red-light);
}
.lb-st.lb-now b {
  color: var(--c-red-dark);
}
/* Sold in a shop: a dot after the field's name, explained in the legend. */
.lb-shopdot {
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: 3px;
  border-radius: 50%;
  background: var(--c-primary);
  vertical-align: 2px;
}

/* Nachproduzieren: one row per species, what it lacks as chips, one button. */
.np-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--c-border-light);
}
.np-row:last-of-type {
  border-bottom: 0;
}
.np-dot {
  flex: 0 0 10px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
}
.np-dot.np-now {
  background: var(--c-red-strong);
}
.np-dot.np-low {
  background: var(--c-amber-strong);
}
.np-main {
  flex: 1 1 auto;
  min-width: 0;
}
.np-what {
  font-weight: 600;
  overflow-wrap: anywhere;
}
.np-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 4px;
}
.np-chip {
  padding: 2px 6px;
  border-radius: 6px;
  background: var(--c-amber-light);
  color: var(--c-amber-dark);
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 600;
  white-space: nowrap;
}
.np-chip.np-now {
  background: var(--c-red-light);
  color: var(--c-red-dark);
}
.np-go {
  flex: 0 0 auto;
  white-space: nowrap;
}
.np-more {
  margin-top: 8px;
}
.np-foot {
  margin-top: 6px;
  color: var(--c-text-muted);
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.45;
}

/* The species sheet (m-lb). */
.lb-prog {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 8px 0 6px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--c-border-light);
  cursor: pointer;
}
.lb-prog input {
  width: auto;
  margin-top: 3px;
}
.lb-prog small {
  display: block;
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
  line-height: 1.35;
}
.lb-line {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--c-border-light);
}
.lb-line-l {
  flex: 1 1 auto;
  min-width: 0;
}
.lb-line-l .lb-info {
  display: block;
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
  line-height: 1.35;
}
.lb-line.lb-under .lb-line-l b {
  color: var(--c-amber-dark);
}
.lb-line.lb-now .lb-line-l b {
  color: var(--c-red-dark);
}
.lb-shop {
  display: inline-block;
  padding: 1px 4px;
  border-radius: 4px;
  background: var(--c-primary-light);
  color: var(--c-primary);
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 700;
}
.lb-step {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 4px;
}
.lb-step b {
  min-width: 48px;
  font-size: 1.0625rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
#m-lb-make[hidden] {
  display: none;
}
/* What to make: 0 is the start, not a figure, so it stays quiet. */
.lb-step b.lb-null {
  color: var(--c-text-muted);
  font-weight: 400;
}
/* The target sits under the stock as a small link; a tap opens its own
   minus and plus, smaller than the one for what to make. */
.lb-soll-btn {
  display: inline-block;
  margin-top: 2px;
  padding: 4px 0;
  border: 0;
  background: none;
  color: var(--c-primary);
  font: inherit;
  font-size: max(0.75rem, var(--fs-min));
  cursor: pointer;
}
.lb-step-soll {
  display: flex;
  margin-top: 2px;
}
.lb-step-soll b {
  min-width: 40px;
  font-size: 0.9375rem;
}
/* Delete sits next to the target link, in the same quiet style, in red. */
.lb-weg-btn {
  margin-left: 14px;
  color: var(--c-red-dark);
}
/* What is gone: one row per culture, tap to mark. Rows are list items, so
   they span the sheet; the delete button below sizes to its words. */
.lb-weg {
  padding: 4px 0 10px;
  border-bottom: 1px solid var(--c-border-light);
}
.lb-weg-id {
  display: flex;
  width: 100%;
  min-height: 44px;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 0 0 4px;
  padding: 6px 10px;
  border: 1px solid var(--c-border);
  border-radius: 6px;
  background: var(--c-surface, #fff);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.lb-weg-id span {
  min-width: 0;
  font-family: monospace;
  font-size: max(0.8125rem, var(--fs-min));
  overflow-wrap: anywhere;
}
.lb-weg-id small {
  flex: 0 0 auto;
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
}
.lb-weg-id.on {
  border-color: var(--c-red-dark);
  background: var(--c-red-light, #fee2e2);
  text-decoration: line-through;
}
.lb-weg-id:disabled {
  cursor: default;
  opacity: 0.6;
}
/* Admin only: the reason goes under the ID, so the ID stays on one line. */
.lb-weg-id:disabled {
  flex-wrap: wrap;
}
.lb-weg-id:disabled small {
  flex: 1 1 100%;
}
.lb-weg .btn {
  margin-top: 4px;
}

/* ── Sortenliste ──
   Kulturbibliothek, Körnerbrut and Chargen: one row per Sorte with its counts;
   a tap opens its entries by group, one line each. Rows are list items and
   span the card; the buttons on top size to their words. */
.sl-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.sl-head .sec {
  margin: 0;
}
.sl-count {
  color: var(--c-text-muted);
  font-size: max(0.8125rem, var(--fs-min));
}
.sl-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 10px 0 6px;
}
.sl-tools .btn[aria-pressed='true'] {
  border-color: var(--c-primary);
  color: var(--c-primary);
}
.sl-sorte {
  border-bottom: 1px solid var(--c-border-light);
}
.sl-sorte.open {
  padding-bottom: 8px;
}
.sl-kopf,
.sl-eintrag,
.sl-mehr {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.sl-kopf {
  min-height: 48px;
  padding: 8px 2px;
}
.sl-name {
  min-width: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.sl-name .sup {
  display: flex;
  margin-top: 2px;
  font-weight: 600;
}
.sl-eintrag.batch-row-flash {
  animation: batch-row-flash 1.5s ease;
}
.sl-chips {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 4px;
}
.sl-chip {
  padding: 2px 6px;
  border-radius: 6px;
  background: var(--c-bg);
  color: var(--c-text-sec);
  font-size: max(0.75rem, var(--fs-min));
  white-space: nowrap;
}
.sl-chip b {
  color: var(--c-text);
}
.sl-sorte.open .sl-kopf {
  color: var(--c-primary);
}
.sl-teil {
  margin: 2px 0 6px;
  padding-left: 10px;
  border-left: 2px solid var(--c-border-light);
}
.sl-teil-t {
  padding: 4px 0;
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 600;
}
.sl-eintrag {
  min-height: 44px;
  padding: 6px 8px;
  border-radius: 6px;
}
.sl-eintrag:hover,
.sl-eintrag:focus-visible {
  background: var(--c-bg);
}
.sl-id {
  min-width: 0;
  font-family: monospace;
  font-size: max(0.8125rem, var(--fs-min));
  overflow-wrap: anywhere;
}
.sl-r {
  flex: 0 0 auto;
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
  white-space: nowrap;
}
.sl-mehr {
  padding: 6px 8px;
  color: var(--c-primary);
  font-size: max(0.8125rem, var(--fs-min));
}
.lb-help,
.lb-tap {
  margin: 0 0 8px;
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
  line-height: 1.35;
}
.lb-ruhen {
  margin: 8px 0 4px;
}
/* Under its floor: the number goes red, not the whole row. The Sorte's name is
   not the problem, the count is. */
.lab-row.under .lab-row-n {
  color: var(--c-red-dark);
}
.lab-row-min {
  font-weight: 400;
  color: var(--c-text-muted);
}
.lab-empty {
  font-style: italic;
}

/* ═══ SORTE IN THE PROGRAMME ═════════════════════════════════════ */
.ms-prog {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-sec);
  user-select: none;
}
.ms-prog input {
  width: auto;
  margin: 0;
  /* Der Schalter ist die Tippfläche, nicht das Label darum: Das Label ist
     `inline-flex` und genau so hoch wie sein Inhalt, also hilft es nicht.
     20 px stand hier mit dem Kommentar „coarse pointers get a real target" —
     das war 20, und die Marken sind 24 (Maus, WCAG 2.5.8 AA) und 44 (Finger).
     Gemessen vom Messstand über das ganze Band auf beiden Achsen; der
     Kommentar hat eine Absicht beschrieben und keine Zahl geprüft.

     `--tap-sm-min` und nicht `--tap-min`: Das ist die 48er-Marke für Chrom,
     das auf jedem Bildschirm mitfährt, nicht die 56er für eine Feld-Fläche.
     Am Rechner sind es 24 px, in der Hand 48. */
  min-width: max(20px, var(--tap-sm-min));
  min-height: max(20px, var(--tap-sm-min));
}
tr.ms-off td {
  opacity: 0.6;
}
tr.ms-off .ms-prog {
  opacity: 1;
}

/* ═══ BATCH ROW FLASH ═══════════════════════════════════════════ */
.batch-row-flash > td {
  animation: batch-row-flash 1.5s ease;
}
@keyframes batch-row-flash {
  0% {
    background: var(--c-amber-light);
  }
  100% {
    background: transparent;
  }
}

/* ═══ OVERVIEW KPI CARDS ════════════════════════════════════════ */
.ov-kpi-group-label {
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-text-muted);
  margin-bottom: 8px;
}
.ov-kpi-card {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: 14px 16px;
  box-shadow: var(--shadow);
  display: flex;
  align-items: flex-start;
  gap: 12px;
  transition: box-shadow 0.15s;
}
.ov-kpi-card:hover {
  box-shadow: var(--shadow-md);
}
.ov-kpi-icon {
  width: 38px;
  height: 38px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.ov-kpi-body {
  min-width: 0;
  flex: 1;
}
.ov-kpi-value {
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.1;
  margin-bottom: 2px;
}
.ov-kpi-label {
  font-size: max(0.75rem, var(--fs-min));
  font-weight: 600;
  color: var(--c-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ov-kpi-sub {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
  margin-top: 1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The shared confirm dialog (#m-confirm) uses textContent for body, so \n
   in the i18n string would otherwise collapse. pre-line keeps newlines while
   still wrapping long single lines. */
#m-body {
  white-space: pre-line;
}

/* ═══ MODALS ════════════════════════════════════════════════════ */
.modal-bg {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.5);
  backdrop-filter: blur(4px);
  z-index: 200;
  align-items: center;
  justify-content: center;
  /* overscroll-behavior:contain still prevents the underlying page from
     bouncing when the modal scrolls past its limits. The previous
     `touch-action:none` here disabled scrolling on the modal content too
     on iOS Safari — workers couldn't scroll a long bag-info modal. The
     allow-vertical-pan rule lives on .modal itself below. */
  overscroll-behavior: contain;
}
.modal-bg.open {
  display: flex;
}
#m-confirm {
  z-index: 210;
}
/* Die Zielliste über der Kamera. Beide sind .modal-bg mit z-index 200, und bei
   Gleichstand malt das spätere Element oben — m-camscan steht in index.html
   hinter m-move-batch. Die Liste, die das "Nach"-Feld der Kamera öffnete, ging
   darum HINTER dem Kamerabild auf: der Tipp tat scheinbar nichts, und der
   nächste Tipp daneben schloss die Kamera. Unter m-confirm (210) bleibt sie. */
body:has(#m-camscan.open) #m-move-batch {
  z-index: 205;
}
.modal {
  background: var(--c-surface);
  border-radius: 16px;
  padding: var(--pad-modal);
  width: 92%;
  max-width: 480px;
  max-height: 92vh;
  max-height: min(92vh, calc(100dvh - 32px));
  overflow-y: auto;
  /* Allow vertical pan inside the modal even when the parent .modal-bg
     would otherwise suppress touch gestures on iOS. */
  touch-action: pan-y;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.2);
}
.modal h3 {
  font-size: 1rem;
  font-weight: 700;
  margin-bottom: 10px;
  color: var(--c-text);
}
.modal p {
  font-size: 0.875rem;
  color: var(--c-text-sec);
  margin-bottom: 14px;
  line-height: 1.6;
}
/* The question in a confirm dialog can be several lines — the stock shortage
   lists one line per material — and it arrives as a string with \n in it,
   because that is the shape it had when window.confirm() rendered it. Collapsed
   to spaces, a three-material shortage reads as one run-on sentence. */
#m-body {
  white-space: pre-line;
}
.modal-btns {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  margin-top: 16px;
}
/* Die Knopfreihe unter dem Ansatz-Fenster.
   Sie kam mit einem Knopf aus und brauchte deshalb nie eine Regel. Seit die
   Ist-Feuchte danebensteht, sind es zwei, und .btn ist inline-flex: appendChild
   setzt keinen Textknoten dazwischen, also stossen sie ohne Abstand aneinander
   und werden auf dem Telefon zu einem Ziel, bei dem der Daumen den falschen
   Treffer nicht bemerkt. Umbruch statt Quetschen, damit der zweite Knopf auf
   einem schmalen Schirm eine Zeile tiefer geht, statt den ersten zu stauchen. */
#si-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* Die Wochensumme im Rhythmus-Fenster.
   Sie klebt am unteren Rand, weil sie die Antwort auf die Frage ist, die man
   beim Tippen hat ("bin ich bei 70?"). Sieben Wochentage mit Menge, Sorte und
   Notiz sind auf einem Telefon höher als der Schirm, und eine Summe unter
   diesen sieben Zeilen ist genau dann nicht zu sehen, wenn man sie braucht.
   .modal scrollt selbst (overflow-y: auto), also ist es dessen Scrollbereich,
   an dem hier geklebt wird; unten angekommen löst sie sich und die letzten
   Zeilen des Fensters schieben sich normal darüber. */
.rhythm-total {
  position: sticky;
  bottom: 0;
  margin-top: 10px;
  padding: 8px 0 2px;
  background: var(--c-surface);
  border-top: 0.5px solid var(--c-border);
}
.rhythm-total-sum {
  font-weight: 600;
}
.rhythm-total-hint {
  color: var(--c-text-muted);
  margin-top: 3px;
}

/* Eine eigene Aufgabe in der Wochenspalte: Kästchen, Name, ein Tipp.

   Randlos und ohne Fläche — das ist KEIN auf volle Breite gezogener Knopf,
   sondern eine Zeile, die man antippen kann, genau wie die Mengenzahl zwei
   Zeilen darüber (_rhythmTargetBtn) es schon ist. Wer hier eine Fläche und
   einen Rahmen einzieht, macht daraus den breitgetretenen Knopf, den es in
   diesem Blatt nirgends geben soll.

   Die Breite braucht sie trotzdem: ohne sie kann der Name in einer 170px-Spalte
   nicht mit Auslassungspunkten enden, und ein umgebrochener Aufgabenname
   schiebt die sieben Spalten auf verschiedene Höhen. */
.wk-chore {
  display: flex;
  align-items: center;
  gap: 5px;
  width: 100%;
  min-height: var(--tap-sm);
  margin-bottom: 3px;
  padding: 3px 3px 3px 5px;
  border: 0;
  border-left: 2px solid var(--c-purple-dark);
  border-radius: 0 4px 4px 0;
  background: none;
  font-family: inherit;
  font-size: inherit;
  text-align: left;
  cursor: pointer;
}
.wk-chore:hover {
  background: var(--c-bg);
}
/* In em, nicht in Pixeln: das Kästchen wächst mit der Zeile, und der Haken
   darin bleibt bei der Schriftgröße der Zeile statt bei einer eigenen. Eine
   feste Zahl hier wäre eine Schriftgröße, an die der Boden der Handseite nicht
   herankommt — genau das, was scripts/mobile-audit.js zählt. */
.wk-chore-box {
  width: 1.15em;
  height: 1.15em;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1.5px solid var(--c-text-muted);
  border-radius: 3px;
  line-height: 1;
  color: var(--c-surface);
}
.wk-chore.done .wk-chore-box {
  background: var(--c-green-dark);
  border-color: var(--c-green-dark);
}
.wk-chore-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wk-chore.done .wk-chore-name {
  color: var(--c-text-muted);
  text-decoration: line-through;
}
/* Versäumt: derselbe rote Streifen, den eine überfällige Zeile überall trägt. */
.wk-chore.late {
  border-left-color: var(--c-red-dark);
}
.wk-chore.late .wk-chore-name {
  color: var(--c-red-dark);
  font-weight: 600;
}

/* Die eigenen Aufgaben, unter der Wochensumme. Eine Zeile je Aufgabe: Name,
   Takt, und der Weg hinaus. Der Name darf schrumpfen, Takt und Knopf nicht —
   ein umgebrochener Takt neben einem abgeschnittenen Namen liest sich als
   zwei Zeilen, die nicht zusammengehören. */
.rhythm-own {
  margin-top: 14px;
  padding-top: 10px;
  border-top: 0.5px solid var(--c-border);
}
.rhythm-own[hidden] {
  display: none;
}
.rhythm-own-head {
  font-weight: 600;
}
.rhythm-own-hint {
  color: var(--c-text-muted);
  margin: 2px 0 8px;
}
.rhythm-own-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
}
.rhythm-own-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rhythm-own-when {
  color: var(--c-text-muted);
  flex-shrink: 0;
}
/* Eine Aufgabe in Bearbeitung: dieselben drei Felder wie beim Anlegen, also
   dieselbe Umbruchregel. */
.rhythm-own-edit {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 6px 0;
  border-top: 0.5px solid var(--c-border);
  border-bottom: 0.5px solid var(--c-border);
}
.rhythm-own-edit input {
  flex: 1 1 100%;
  min-width: 0;
}
.rhythm-own-edit select {
  flex: 1 1 96px;
  min-width: 0;
}
.rhythm-own-edit .btn {
  flex: 0 0 auto;
}
/* Pausiert: bleibt lesbar, aber nicht mehr im Vordergrund. */
.rhythm-own-row.ruht .rhythm-own-name {
  color: var(--c-text-muted);
}

/* Auf dem Telefon umbrechen statt vier Felder in eine 300px-Zeile zu quetschen:
   der Name nimmt die volle Breite, Takt und Wochentag teilen sich die nächste. */
.rhythm-own-add {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
/* Auch hier keine eigene Mindesthöhe: die allgemeine input-Regel hat schon
   64px, und eine Klasse davor hätte sie verkleinert. */
.rhythm-own-add input {
  flex: 1 1 100%;
  min-width: 0;
}
.rhythm-own-add select {
  flex: 1 1 96px;
  min-width: 0;
}
.rhythm-own-add .btn {
  flex: 0 0 auto;
}

/* Die Menge eines einzelnen Tages: Regel und Ausnahme untereinander.
   Beschriftung links, Zahl rechts — auf dem Telefon bleibt das nebeneinander,
   weil die Beschriftung hier die eigentliche Auskunft ist ("gilt für jeden
   Montag" gegen "nur für den 25.08.") und ein Umbruch sie von ihrem Feld
   trennen würde. Das Feld ist schmal genug, dass beides passt. */
.dayqty-sub {
  color: var(--c-text-muted);
  margin: -4px 0 14px;
}
.dayqty-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
  cursor: pointer;
}
.dayqty-lbl {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.dayqty-hint {
  color: var(--c-text-muted);
}
/* Keine eigene Mindesthöhe. Hier stand `min-height: var(--tap-sm)` mit der
   Begründung, die allgemeine input-Regel setze keine — sie setzt aber
   `min-height: 64px` (ganz oben in diesem Blatt), und eine Klasse schlägt den
   Elementselektor. Die Zeile, die als größere Tippfläche gedacht war, machte
   das Feld also von 64px auf 48px am Telefon und rund 39px am Rechner: die
   genaue Umkehrung ihrer eigenen Absicht. Ohne sie gilt wieder die 64. */
.dayqty-in {
  width: 84px;
  flex-shrink: 0;
  padding: 8px;
  border: 1px solid var(--c-border);
  border-radius: 6px;
  font-family: inherit;
  text-align: right;
}
/* Ein gesperrtes Feld muss als gesperrt zu erkennen sein, sonst tippt jemand
   hinein und wundert sich. Der Grund steht in der Beschriftung darunter. */
.dayqty-in:disabled {
  background: var(--c-bg);
  color: var(--c-text-muted);
  cursor: not-allowed;
}
.dayqty-sum {
  font-weight: 700;
  padding-top: 10px;
  border-top: 0.5px solid var(--c-border);
}

/* ═══ SUCHE ════════════════════════════════════════════════════════════════
   The palette. It is a .modal-bg, so it inherits the backdrop, the blur and
   the z-index; what it overrides is the placement. A centred dialog that grows
   downwards moves its own first row away from the eye on every keystroke, so
   this one is pinned near the top and grows into empty space instead. */
.gs-bg {
  align-items: flex-start;
  justify-content: center;
  padding: 10vh 16px 16px;
}
.gs-box {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 560px;
  /* Bounded, so the list scrolls inside the box instead of running off the
     bottom of the screen. It never reaches this on a desk — seven results and a
     footer come to roughly 450px. The phone value is in the media query below,
     and it is in dvh for a reason spelled out there. */
  max-height: 100%;
  background: var(--c-surface);
  border-radius: 16px;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
  overflow: hidden;
}
.gs-field {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--c-border);
  color: var(--c-text-muted);
}
/* The field carries no border of its own — the row it sits in is the border.
   !important is not needed: the input rules in this file are all element or
   class selectors, and .gs-field input is (0,2,1). */
.gs-field input {
  flex: 1;
  min-width: 0;
  border: none;
  background: transparent;
  padding: 0;
  font-size: max(1rem, var(--fs-min));
  color: var(--c-text);
}
.gs-field input:focus {
  outline: none;
  box-shadow: none;
}
.gs-esc {
  flex-shrink: 0;
  border: 1px solid var(--c-border);
  background: transparent;
  color: var(--c-text-muted);
  border-radius: var(--radius-sm);
  padding: 3px 8px;
  font-family: monospace;
  font-size: max(0.6875rem, var(--fs-min));
  cursor: pointer;
  /* Both floors, not just the tall one. On a phone the word "esc" is swapped
     for a ✕ and this stops being a reminder and starts being the only way out
     that a thumb can reach — at which point 24px tall and as wide as one glyph
     is not a target. */
  min-height: max(24px, var(--tap-sm-min));
  min-width: max(28px, var(--tap-sm-min));
}
.gs-results {
  overflow-y: auto;
  /* The field and the footer stay put; this is the part that gives. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.gs-esc-x {
  display: none;
}
/* Out of sight, still announced. Not display:none and not visibility:hidden —
   both take an element out of the accessibility tree, which is the one thing
   this element exists to be in. */
.gs-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
.gs-group {
  padding: 8px 14px 3px;
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
}
.gs-row {
  display: block;
  padding: 7px 14px;
  cursor: pointer;
  min-height: max(0px, var(--tap-sm-min));
  box-sizing: border-box;
}
.gs-row-t,
.gs-row-s {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gs-row-t {
  font-size: max(0.875rem, var(--fs-min));
  color: var(--c-text);
}
.gs-mono {
  font-family: monospace;
}
.gs-row-s {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-sec);
}
/* Selection is one thing, and it is the same thing whether the keyboard or the
   mouse put it there. Pairing .gs-on with :hover here said that and did the
   opposite: arrow down to row two, leave the pointer over row four, and two
   rows are lit while Enter opens one of them. The pointer now moves the
   selection instead (gsInit's mousemove handler), so there is one rule and one
   highlight. */
.gs-row.gs-on {
  background: var(--c-primary-light);
}
.gs-row mark {
  background: var(--c-amber-light);
  color: inherit;
  border-radius: 3px;
}
.gs-empty,
.gs-more {
  padding: 14px;
  font-size: max(0.8125rem, var(--fs-min));
  color: var(--c-text-sec);
}
.gs-more {
  padding: 7px 14px 10px;
  color: var(--c-text-muted);
}
.gs-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 9px 14px;
  border-top: 1px solid var(--c-border);
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
}
.gs-hint {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Where the search landed. .batch-row-flash only reaches table rows (the
   animation is on its td children, because a tr's own background is painted
   under the cells); zones are divs, so this pairs the two selectors and reuses
   the keyframes rather than writing a second amber fade. */
.gs-flash,
.gs-flash > td {
  animation: batch-row-flash 1.5s ease;
}
/* The sidebar's own entry point. It is an .sb-btn so the collapse rules already
   know what to do with it; what the wrapper adds is .sb-nav's own horizontal
   padding, so the button lines up with the entries it sits above rather than
   running 10px wider on each side. */
.sb-search-wrap {
  padding: 12px 10px 0;
}
.sb-search-key {
  margin-left: auto;
  border: 1px solid var(--c-border);
  border-radius: 4px;
  padding: 1px 5px;
  font-family: monospace;
  font-size: max(0.625rem, var(--fs-min));
  color: var(--c-text-muted);
}
.sidebar.sb-collapsed .sb-search-key {
  display: none;
}
.mobile-search {
  display: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  background: transparent;
  color: var(--c-text-sec);
  border-radius: var(--radius-sm);
  cursor: pointer;
  padding: 0;
  /* Same pair as .mobile-bell, and for the same reason: 36 is the desk value
     nothing renders, the floor is what a thumb gets. */
  min-width: max(36px, var(--tap-sm-min));
  min-height: max(36px, var(--tap-sm-min));
}
.mobile-search:hover {
  background: var(--c-border-light);
  color: var(--c-text);
}
@media (max-width: 768px) {
  /* Nearer the top: the on-screen keyboard takes the lower half, and 10vh of a
     phone is a strip of backdrop nobody can reach past. */
  .gs-bg {
    padding: 12px 10px 10px;
  }
  .gs-box {
    max-width: none;
    /* dvh, not vh, and not the 100% it used to be. .gs-bg is position:fixed
       inset:0, so 100% of it is the window — and with the on-screen keyboard up
       the window is still the whole window as far as vh and a fixed backdrop are
       concerned. The bound therefore never applied on the one device it was
       written for: a phone with the keyboard open has around 400px left, which
       is three rows, and the other four ran off the bottom with no way to reach
       them. Same reason and same fix as .wkf-list, which says it at length.
       22px is this block's own vertical padding on .gs-bg. */
    max-height: calc(100dvh - 22px);
  }
  /* The rows are what a thumb aims at, so they get the chrome floor rather than
     the 7px a mouse is happy with. */
  .gs-row {
    padding: 10px 14px;
  }
  /* "↑↓ wählen · ↵ öffnen" over a device with no arrow keys and no Enter is a
     line that describes somebody else's machine — and it is 40px of a screen
     that has a keyboard sitting on half of it. The hint beside it says which
     result Enter would open, which is the same sentence again. */
  .gs-foot {
    display: none;
  }
  /* And the same for the word on the dismiss button. It is a real tap target
     here rather than a reminder, so it says what tapping it does. */
  .gs-esc-key {
    display: none;
  }
  .gs-esc-x {
    display: inline;
  }
  .mobile-search {
    display: flex;
  }
  /* The title takes the slack so the magnifier and the bell sit together at the
     right edge, instead of the bell's margin-left:auto splitting it with them. */
  .mobile-topbar .sb-logo {
    flex: 1;
  }
}

/* Destination chips open the shared zone picker (for when a location barcode
   can't be scanned), so they need to read as tappable — and be tappable. They
   carried a cursor and nothing else, which is a mouse affordance on a control
   whose whole purpose is the case where scanning failed and a gloved hand has
   to pick the zone by hand. */
#chip-to,
#cam-chip-to {
  cursor: pointer;
  min-height: var(--tap);
}

/* ═══ STOCKTAKE SCAN FLASH ═════════════════════════════════════
   Per-bag confirmation during a zone check. The normal success flash paints
   #scan-overlay, which sits behind the camera and is therefore invisible while
   scanning — this flashes the camera view itself so a scan is unmistakable
   without opening anything. */
#m-camscan.cam-flash-ok::after,
#m-camscan.cam-flash-err::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
  animation: camFlash 0.28s ease-out;
}
#m-camscan.cam-flash-ok::after {
  box-shadow: inset 0 0 0 6px #16a34a;
  background: rgba(22, 163, 74, 0.16);
}
#m-camscan.cam-flash-err::after {
  box-shadow: inset 0 0 0 6px #dc2626;
  background: rgba(220, 38, 38, 0.16);
}
@keyframes camFlash {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  #m-camscan.cam-flash-ok::after,
  #m-camscan.cam-flash-err::after {
    animation-duration: 0.01s;
  }
}

/* ═══ UNDO SNACKBAR ════════════════════════════════════════════ */
/* Bottom-centered "Rückgängig" bar for reversible → Fruchtung moves. */
.undo-bar {
  position: fixed;
  left: 50%;
  bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  transform: translate(-50%, 150%);
  z-index: 900;
  display: flex;
  align-items: center;
  gap: 14px;
  max-width: min(90vw, 420px);
  padding: 11px 12px 11px 16px;
  border-radius: 12px;
  background: #14201a;
  color: #eaf3ed;
  font-size: 0.8125rem;
  line-height: 1.3;
  box-shadow: 0 12px 30px -8px rgba(0, 0, 0, 0.5);
  opacity: 0;
  pointer-events: none;
  transition:
    transform 0.2s ease,
    opacity 0.2s ease;
}
/* The error skin. Same bar, same place, same size — only the colour and the
   role change, because a message that moves to a different corner depending on
   whether it is good news is a message the eye has to hunt for. Dark red rather
   than the app's --c-red: this bar sits on #14201a in both themes, and a light
   surface colour on it is unreadable. */
.undo-bar.is-err {
  background: #3a1618;
  color: #ffe1e1;
}
.undo-bar.show {
  transform: translate(-50%, 0);
  opacity: 1;
  pointer-events: auto;
}
.undo-bar #undo-msg {
  flex: 1;
  min-width: 0;
}
.undo-bar #undo-btn {
  flex: none;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: #7ee2a1;
  font-weight: 700;
  font-size: 0.8125rem;
  padding: 7px 14px;
  border-radius: 8px;
  cursor: pointer;
  /* 31.5px, on the control that takes back a scan that went to the wrong zone.
     Chrome floor rather than the Feld one: the bar is a transient overlay and
     56px of it would cover a row of whatever is underneath. */
  min-height: var(--tap-sm-min);
  padding-inline: 18px;
}
.undo-bar #undo-btn:hover {
  background: rgba(255, 255, 255, 0.08);
}
/* On phones the camera scan FAB sits bottom-right — lift the undo bar above it.
   56px is the FAB's height and 12px the gap, so this tracks --fab-bottom instead
   of restating a number derived from it: when the shelf moved to clear the
   bottom nav, the hand-computed 92px did not, and the toast landed on the FAB. */
@media (pointer: coarse) and (hover: none) {
  .undo-bar {
    bottom: calc(var(--fab-bottom) + 56px + 12px + env(safe-area-inset-bottom, 0px));
  }
}

/* "Neue Version der App da" — oben, weil unten schon der Rückgängig-Balken, der
   Scannen-Knopf und die Leiste stehen. Er schwebt, statt die Seite
   herunterzuschieben: er steht meist nur Sekunden, bis die App sich selbst neu
   lädt. Die [hidden]-Regel ist nötig, weil display:flex die des Browsers schlägt. */
.update-bar[hidden] {
  display: none;
}
.update-bar {
  position: fixed;
  left: 50%;
  top: calc(8px + env(safe-area-inset-top, 0px));
  transform: translateX(-50%);
  z-index: 950;
  display: flex;
  align-items: center;
  gap: 14px;
  width: max-content;
  max-width: min(92vw, 420px);
  padding: 6px 6px 6px 16px;
  border-radius: 12px;
  background: #1e3a5f;
  color: #e0ecff;
  font-size: max(0.875rem, var(--fs-min));
  font-weight: 600;
  line-height: 1.3;
  box-shadow: 0 12px 30px -8px rgba(0, 0, 0, 0.5);
}
.update-bar #update-btn {
  flex: none;
  min-height: var(--tap-sm-min);
  padding: 0 16px;
  border: none;
  border-radius: 8px;
  background: #e0ecff;
  color: #1e3a5f;
  font-family: inherit;
  font-size: inherit;
  font-weight: 700;
  cursor: pointer;
}
/* Über der Kamera nicht: dort wird gescannt, und neu geladen wird ohnehin erst,
   wenn sie zu ist. */
body:has(#m-camscan.open) .update-bar {
  display: none;
}

/* Batch tasks and Alerts: side by side on a desk, stacked in a hand. These four
   were an inline style, and an inline value outranks every rule in the sheet — so
   the phone block below could turn the row into a column and could not touch the
   alignment, leaving both cards at the width of their own text. Measured at 375px:
   137px of card inside a 343px page. */
/* Der Farbleitfaden erklärt die Zeichen in der Aufgabenliste. Er gehört unter
   die Liste und nicht über die halbe Karte: rechts, klein, zurückgenommen —
   und nur eingeblendet, wenn eine Liste da ist (renderDashBatchTasks). Hier
   steht bewusst kein display, damit das hidden-Attribut wirkt. */
/* Der Durchschnitt wird gerechnet, nicht getippt — also sieht er auch nicht wie
   ein Eingabefeld aus. Gleiche Höhe wie ein Feld, damit die Reihe nicht
   springt, aber ohne Rahmen und ohne Cursor. */
.avg-wert {
  font-size: 0.8125rem;
  padding: 5px 8px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--c-text);
}
.avg-einheit {
  font-weight: 400;
  color: var(--c-text-muted);
  margin-left: 2px;
}

/* Über der Grenze: schraffiert statt nur rot. Rot war schon dreimal vergeben,
   und eine Schraffur liest auch, wer Farben schlecht unterscheidet. */
.kap-voll {
  background-image: repeating-linear-gradient(45deg, rgb(255 255 255 / 0.5) 0 3px, rgb(255 255 255 / 0) 3px 6px);
}

.dash-top-row {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  /* Folgt den Kacheln; eine Liste, die an der letzten Kachel klebt, liest sich
     als deren Fortsetzung. */
  margin-top: 16px;
  margin-bottom: 10px;
}

/* ═══ DESKTOP: batch tasks own a full-width row ════════════════ */
/* The move list is the primary card. On wide screens it takes its own
   row so its two-button rows aren't crushed into a quarter-column; the
   harvest / alerts / split cards flow onto the row below. Mobile keeps
   the single-column stack from the max-width:768px block. */
@media (min-width: 769px) {
  .dash-top-row {
    flex-wrap: wrap;
  }
  .dash-top-row #dash-batch-tasks-card {
    flex: 1 1 100% !important;
  }
}

/* ═══ TABLET ═══════════════════════════════════════════════════ */
@media (max-width: 1024px) {
  .g4,
  .g6 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .rack-grid.rack-grid-5col {
    grid-template-columns: repeat(3, 1fr);
  }
  .tent-columns {
    grid-template-columns: repeat(2, 1fr);
  }
  .scan-modal {
    width: 90vw;
    max-width: 90vw;
    height: 85vh;
    max-height: 85vh;
  }
}

/* ═══ MOBILE ════════════════════════════════════════════════════ */
@media (max-width: 768px) {
  .sidebar {
    transform: translateX(-100%);
  }
  .sidebar.sb-open {
    transform: translateX(0);
  }
  .mobile-topbar {
    display: flex;
  }
  /* The title gives, the chrome does not. The bar holds the page's name now, and
     the longest of them — pt `nav.workSteps`, "Etapas de trabalho" — is 4px over a
     320px bar. Without these the overflow pushes the bell and the sync dot off the
     right edge instead; .sb-logo carries flex-shrink: 0, and both it and the span
     need min-width: 0 before an ellipsis can happen at all. */
  .mobile-topbar .sb-logo {
    flex-shrink: 1;
    min-width: 0;
  }
  #topbar-title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .content-wrap {
    margin-left: 0;
  }
  .sb-is-collapsed .content-wrap {
    margin-left: 0;
  }
  /* Constrain inline-sized inputs so they can't overflow narrow viewports */
  input[style*='width:'],
  select[style*='width:'] {
    max-width: 100%;
  }
  /* Eight fields the rule above was written for and could not reach: their cap
     was an inline `max-width`, which outranks every rule in this sheet. It is a
     custom property now, so the cascade decides again — same device as
     .fs-floor and --fs-own. Two of the eight declare no inline width at all, so
     the attribute selector does not match them either; this is what covers them. */
  .w-cap {
    max-width: 100%;
  }
  /* WCAG-size touch targets for calendar toolbar on mobile */
  .cal-nav-btn,
  .cal-print-btn {
    width: 44px;
    height: 44px;
    font-size: 1.125rem;
  }
  .cal-today-btn,
  .cal-add-btn {
    min-height: 44px;
  }
  .cal-vbtn {
    min-height: 44px;
  }
  .scan-modal {
    width: 95vw;
    max-width: 95vw;
    max-height: 90vh;
    max-height: 90dvh;
    height: 90vh;
    height: 90dvh;
    border-radius: 12px;
    padding: 0;
  }
  .scan-overlay {
    align-items: center;
  }
  .scan-modal-log {
    max-height: 40vh;
    max-height: 40dvh;
  }
  /* Narrower side padding so four tabs fit; the size stays where the token put
     it. Shrinking type at a narrow breakpoint is the exact move the token layer
     replaced — and this one was on the tab strip of the screen that is only
     ever read at arm's length. */
  .scan-tab {
    padding: 10px 10px;
  }
  .g3,
  .g4,
  .g6 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .g2 {
    grid-template-columns: minmax(0, 1fr);
  }
  th,
  td {
    padding: 7px 6px;
  }
  .met-v {
    font-size: 1.375rem;
  }
  .cal-cell {
    min-height: 72px;
  }
  .cal-toolbar {
    flex-direction: column;
    align-items: stretch;
  }
  .cal-toolbar-left,
  .cal-toolbar-right {
    justify-content: center;
  }
  .cal-title {
    font-size: 1rem;
    margin-left: 4px;
  }
  /* Bigger day cells on phone (audit Section 1.14: gloved hands can't
     reliably hit 24x24 — adjacent dates get fat-fingered). 36x36 keeps the
     7-column grid usable on 360px (7×36 = 252px) while leaving 100+px for
     padding/events. */
  .cal-day {
    width: 36px;
    height: 36px;
    font-size: 0.8125rem;
  }
  .cal-event {
    padding: 1px 5px;
  }
  .cal-week-hdr-cell .wk-day-num {
    font-size: 1rem;
  }
  /* The drawer stops repeating the bottom bar. app.js marks these from the same
     list that wires the bar's buttons, so the two cannot drift apart: 19 rows
     and 868px of content in a 655px viewport becomes 14 rows that fit, and the
     five destinations that were pushed below the fold were the five the bar
     shows permanently anyway. A group heading whose entries are all in the bar
     is marked too, or "Arbeiten" would sit alone over the gap. */
  .sb-in-bottom-nav {
    display: none;
  }
  /* Both of these describe the grids, and below this breakpoint there are no
     grids. The toggle would offer three views that all render the agenda; the
     legend is eight colour keys for dots whose meaning every agenda row spells
     out in words next to the label. Hidden rather than shrunk — the eight
     legend spans were the last sub-floor type in the app at 11px, and the
     honest fix for a key nobody needs is not a bigger key. */
  .cal-view-toggle,
  .cal-legend {
    display: none;
  }
  .dash-top-row {
    flex-direction: column;
    align-items: stretch;
  }
  .rack-grid.rack-grid-5col {
    grid-template-columns: repeat(2, 1fr);
  }
  .tent-columns {
    grid-template-columns: 1fr;
  }
  .modal {
    width: 96%;
  }
  .scan-modal {
    width: 95vw;
    height: 90vh;
    height: 90dvh;
    max-height: 90vh;
    max-height: 90dvh;
    max-width: 95vw;
    border-radius: 12px;
    margin: 0;
  }
  .scan-overlay.open {
    align-items: center;
  }
  .scan-modal-log {
    min-height: 200px;
  }
}
@media (max-width: 480px) {
  .dash-search-inline {
    width: 140px;
  }
  .card {
    border-radius: var(--radius-sm);
  }
  .g2,
  .g3,
  .g4,
  .g6 {
    grid-template-columns: minmax(0, 1fr);
  }
  .cal-cell {
    min-height: 60px;
    padding: 4px;
  }
  .todo-row {
    padding: 10px;
    gap: 8px;
  }
  .scan-modal {
    padding: 12px;
  }
  .scan-modal-chips {
    gap: 4px;
  }
  .chip {
    min-width: 44px;
    padding: 4px 6px;
  }
  .scan-modal-btns {
    flex-wrap: wrap;
    justify-content: center;
  }
}

/* ═══ LANDSCAPE (short viewports) ═══════════════════════════════ */
@media (max-height: 500px) and (orientation: landscape) {
  .modal {
    max-height: calc(100dvh - 20px);
    padding: 16px 20px;
  }
  .modal h3 {
    margin-bottom: 6px;
  }
  .scan-modal {
    height: auto;
    max-height: calc(100dvh - 20px);
  }
  .scan-modal-log {
    max-height: 30vh;
  }
  .cal-toolbar {
    flex-direction: row;
    flex-wrap: wrap;
  }
  .content-wrap {
    min-height: 100dvh;
  }
}

/* ═══ PRINT ═════════════════════════════════════════════════════ */
@media print {
  .sidebar,
  .mobile-topbar,
  .scan-toast-inline,
  #harvest-panel,
  .no-print,
  .sb-overlay {
    display: none !important;
  }
  .content-wrap {
    margin-left: 0 !important;
    min-height: 0 !important;
    display: none !important;
  }
  .main {
    padding: 0;
    max-width: 100%;
  }
  .page,
  .sp {
    display: none !important;
  }
  #print-sheet,
  #ref-print-sheet {
    display: block !important;
    padding: 10px;
  }
  body {
    background: #fff;
  }
  .card {
    border: none;
    box-shadow: none;
    padding: 0;
  }

  /* Calendar print */
  .cal-print-page {
    font-family: Arial, Helvetica, sans-serif;
  }
  .cal-print-header {
    margin-bottom: 8px;
    padding-bottom: 6px;
    border-bottom: 2px solid #333;
  }
  .cal-print-body .cal-grid {
    border: 1px solid #999;
    gap: 0;
  }
  .cal-print-body .cal-hdr {
    background: #f0f0f0;
    border-bottom: 1px solid #999;
    padding: 6px 4px;
    font-size: 10px;
  }
  .cal-print-body .cal-cell {
    border: 0.5px solid #ccc;
    min-height: 80px;
    padding: 4px 5px;
  }
  .cal-print-body .cal-cell.today {
    background: #f0faf5;
  }
  .cal-print-body .cal-cell.other {
    background: #f8f8f8;
  }
  .cal-print-body .cal-day {
    font-size: 11px;
    width: auto;
    height: auto;
  }
  .cal-print-body .cal-event {
    font-size: 9px;
    padding: 1px 5px;
    margin-bottom: 2px;
    border-radius: 3px;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .cal-print-body .cal-more {
    font-size: 9px;
  }
  .cal-print-body .cal-week {
    border: 1px solid #999;
  }
  .cal-print-body .cal-week-hdr {
    border-bottom: 1px solid #999;
  }
  .cal-print-body .cal-week-hdr-cell {
    padding: 6px 4px;
    font-size: 10px;
  }
  .cal-print-body .cal-week-hdr-cell .wk-day-num {
    font-size: 16px;
  }
  .cal-print-body .cal-week-body {
    max-height: none;
    overflow: visible;
  }
  .cal-print-body .cal-week-ev {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    font-size: 9px;
  }
  .cal-print-body .cal-week-slot {
    height: 36px;
  }
  .cal-print-body .cal-week-time {
    height: 36px;
    font-size: 9px;
  }
  .cal-print-body .cal-day-view {
    border: 1px solid #999;
  }
  .cal-print-body .cal-day-body {
    max-height: none;
    overflow: visible;
  }
  .cal-print-body .cal-day-ev {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    font-size: 10px;
  }
  .cal-print-body .cal-day-slot {
    height: 36px;
  }
  .cal-print-body .cal-day-time {
    height: 36px;
    font-size: 9px;
  }

  /* Calendar print — task list */
  .cal-print-tasklist .cal-print-header {
    margin-bottom: 12px;
    padding-bottom: 8px;
    border-bottom: 2px solid #333;
  }
  .cal-print-tasklist .cal-print-day {
    margin-bottom: 10px;
    page-break-inside: avoid;
    border: 1px solid #ccc;
    border-radius: 4px;
    overflow: hidden;
  }
  .cal-print-tasklist .cal-print-day.today {
    border-color: #16a34a;
  }
  .cal-print-tasklist .cal-print-day-hdr {
    background: #f0f0f0;
    padding: 6px 10px;
    font-size: 12px;
    font-weight: 700;
    color: #111;
    border-bottom: 1px solid #ccc;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .cal-print-tasklist .cal-print-day.today .cal-print-day-hdr {
    background: #dcfce7;
  }
  .cal-print-tasklist .cal-print-empty {
    padding: 6px 10px;
    font-size: 10px;
    color: #888;
    font-style: italic;
  }
  .cal-print-tasklist .cal-print-list {
    list-style: none;
    margin: 0;
    padding: 0;
  }
  .cal-print-tasklist .cal-print-item {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 5px 10px;
    font-size: 11px;
    color: #111;
    border-bottom: 1px dashed #e5e5e5;
  }
  .cal-print-tasklist .cal-print-item:last-child {
    border-bottom: none;
  }
  .cal-print-tasklist .cal-print-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-top: 4px;
    flex-shrink: 0;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .cal-print-tasklist .cal-print-time {
    min-width: 82px;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: #333;
    flex-shrink: 0;
  }
  .cal-print-tasklist .cal-print-type {
    min-width: 70px;
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: #666;
    padding-top: 1px;
    flex-shrink: 0;
  }
  .cal-print-tasklist .cal-print-label {
    flex: 1;
    font-weight: 500;
  }
  .cal-print-tasklist .cal-print-assignees {
    font-weight: 400;
    color: #555;
    font-size: 10px;
  }
  .cal-print-tasklist .cal-print-desc {
    font-weight: 400;
    color: #555;
    font-size: 10px;
    margin-top: 2px;
  }
}
#print-sheet,
#ref-print-sheet {
  display: none;
}

/* Zone management */
.zone-row {
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--c-surface);
  margin-bottom: 8px;
  position: relative;
}
.zone-row-header {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.zone-row-name {
  font-weight: 600;
  font-size: 0.875rem;
}
.zone-row-racks {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 6px;
}
.zone-rack-chip {
  font-size: max(0.6875rem, var(--fs-min));
  font-family: monospace;
  padding: 2px 8px;
  border-radius: 4px;
  background: var(--c-border-light);
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.zone-rack-del {
  padding: 1px 4px !important;
  font-size: max(0.5625rem, var(--fs-min)) !important;
  margin-left: 4px;
  line-height: 1;
}
.zone-group-header {
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--c-text-muted);
  padding: 14px 4px 6px;
  border-bottom: 1px solid var(--c-border-light);
  margin-bottom: 6px;
}
.zone-group-header:first-child {
  padding-top: 4px;
}
.zone-drag-handle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 22px;
  margin-right: 2px;
  font-size: 0.875rem;
  font-weight: 700;
  letter-spacing: -2px;
  color: var(--c-text-muted);
  cursor: grab;
  user-select: none;
  border-radius: 3px;
}
.zone-drag-handle:hover {
  background: var(--c-border-light);
  color: var(--c-text);
}
.zone-drag-handle:active {
  cursor: grabbing;
}
.zone-row.zone-dragging {
  opacity: 0.4;
}
.zone-row.zone-drop-before {
  box-shadow: inset 0 4px 0 0 var(--c-primary);
}
.zone-row.zone-drop-after {
  box-shadow: inset 0 -4px 0 0 var(--c-primary);
}
.zone-direct-badge {
  background: var(--c-red) !important;
  color: #fff !important;
  font-size: max(0.75rem, var(--fs-min)) !important;
  font-weight: 700 !important;
  padding: 4px 10px !important;
  border-radius: 6px !important;
  box-shadow: 0 0 0 2px var(--c-red-light);
  letter-spacing: 0.3px;
}
.zone-qr-panel {
  border-top: 1px solid var(--c-border-light);
  margin-top: 6px;
  padding-top: 8px;
}
.zone-qr-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.zone-qr-cell {
  border: 1px solid var(--c-border);
  border-radius: 6px;
  padding: 8px;
  text-align: center;
  background: var(--c-surface);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.zone-qr-label {
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  font-family: monospace;
  color: var(--c-text-sec);
}

/* ═══ NOTIFICATIONS (floating inbox top-right) ═══════════════════ */
.notif-fab {
  position: fixed;
  top: 12px;
  right: 16px;
  z-index: 150;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 1px solid var(--c-border);
  border-radius: 50%;
  background: var(--c-surface);
  color: var(--c-text-sec);
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  transition:
    background 0.15s,
    color 0.15s,
    box-shadow 0.15s,
    transform 0.08s;
}
.notif-fab svg {
  width: 24px;
  height: 24px;
}
.notif-fab:hover {
  background: var(--c-border-light);
  color: var(--c-text);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.notif-fab:active {
  transform: translateY(1px);
}
/* When there are unread notifications, make the button prominent and pulse */
.notif-fab:has(.bell-badge:not([hidden])) {
  background: var(--c-primary);
  color: #fff;
  border-color: var(--c-primary);
  animation: notif-pulse 2s ease-in-out infinite;
}
.notif-fab:has(.bell-badge:not([hidden])):hover {
  background: var(--c-primary);
  color: #fff;
  filter: brightness(1.05);
}
@keyframes notif-pulse {
  0%,
  100% {
    box-shadow:
      0 2px 8px rgba(0, 0, 0, 0.08),
      0 0 0 0 rgba(22, 163, 74, 0.55);
  }
  50% {
    box-shadow:
      0 2px 8px rgba(0, 0, 0, 0.08),
      0 0 0 10px rgba(22, 163, 74, 0);
  }
}
@media (prefers-reduced-motion: reduce) {
  /* Disable every named keyframe animation in this stylesheet. The standard
     "*, *::before, *::after { animation-duration: 0.01ms !important }" trick
     is too heavy — it would also kill open/close transitions which are
     functional (they signal state change). Listing the animation-using
     selectors explicitly leaves transitions intact. */
  .notif-fab:has(.bell-badge:not([hidden])),
  .chip-pulse,
  .count-bump .chip-v,
  .cam-chip.ch-pulse,
  .batch-row-flash > td,
  .gs-flash,
  .gs-flash > td {
    animation: none !important;
  }
}
.bell-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: #ef4444;
  color: #fff;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--c-surface);
}
.bell-badge[hidden] {
  display: none;
}

.mobile-bell {
  position: relative;
  display: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  background: transparent;
  color: var(--c-text-sec);
  border-radius: var(--radius-sm);
  cursor: pointer;
  margin-left: auto;
  padding: 0;
  /* 36px is a phone value in a rule the phone is the only reader of — the bell
     is display:none until 768px. The floor lifts it to 48 there and leaves the
     literal alone above, where nothing renders it anyway. The topbar's own
     padding drops by the same 12px (see the 768px block), so the bar stays the
     height it is and only the target grows. */
  min-width: max(36px, var(--tap-sm-min));
  min-height: max(36px, var(--tap-sm-min));
}
.mobile-bell:hover {
  background: var(--c-border-light);
  color: var(--c-text);
}
.mobile-bell .bell-badge {
  top: 2px;
  right: 2px;
  box-shadow: 0 0 0 2px var(--c-surface);
}

.notif-dropdown {
  position: fixed;
  top: 60px;
  right: 16px;
  width: 360px;
  max-width: calc(100vw - 24px);
  max-height: 70vh;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
  z-index: 200;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.notif-dropdown[hidden] {
  display: none;
}
.notif-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  border-bottom: 1px solid var(--c-border-light);
}
.notif-title {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--c-text);
}
.notif-mark-read {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-primary);
  background: transparent;
  border: none;
  cursor: pointer;
  font-family: inherit;
  padding: 2px 4px;
}
.notif-mark-read:hover {
  text-decoration: underline;
}
.notif-list {
  flex: 1;
  overflow-y: auto;
}
.notif-item {
  display: block;
  width: 100%;
  padding: 10px 14px;
  border: none;
  background: transparent;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
  border-bottom: 1px solid var(--c-border-light);
  color: var(--c-text);
}
.notif-item:last-child {
  border-bottom: none;
}
.notif-item:hover {
  background: var(--c-border-light);
}
.notif-item.unread {
  background: var(--c-primary-light);
}
.notif-item.unread:hover {
  background: var(--c-primary-light);
  filter: brightness(0.97);
}
.notif-item-title {
  font-size: 0.8125rem;
  font-weight: 600;
  margin-bottom: 2px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.notif-item.unread .notif-item-title::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--c-primary);
  flex-shrink: 0;
}
.notif-item-body {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-sec);
  margin-bottom: 3px;
}
.notif-item-time {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
}
.notif-empty {
  padding: 24px 14px;
  text-align: center;
  color: var(--c-text-muted);
  font-size: max(0.75rem, var(--fs-min));
}

@media (max-width: 768px) {
  /* On mobile, the mobile-topbar has its own inbox icon — hide the floating FAB */
  .notif-fab {
    display: none;
  }
  .mobile-bell {
    display: inline-flex;
  }
  /* Paid for out of the bar's own padding rather than the screen: 10px around a
     36px bell and 4px around a 48px one are both 56px tall. */
  .mobile-topbar {
    padding-block: 4px;
    padding-top: calc(4px + env(safe-area-inset-top, 0px));
  }
  .notif-dropdown {
    left: 8px;
    right: 8px;
    /* Under the bar, not under the notch. The bar is 56px of content plus
       whatever the display cutout takes — it says so itself, two rules up —
       and this is `fixed`, so its 56px is measured from the physical top of
       the screen, which with viewport-fit=cover is behind the status bar. On a
       phone with no cutout the two agree and the panel sits flush; on one with
       a notch or an island the panel opened 47-59px high and, at z-index 200
       against the bar's 110, covered the bell that had just been tapped. */
    top: calc(56px + env(safe-area-inset-top, 0px));
    bottom: auto;
    width: auto;
    max-width: none;
    /* Same correction at the other end: 80px was the bar plus a margin, and the
       home indicator is not in it. Without the inset the last row of the list
       sits in the strip iOS reserves for its own swipe. */
    max-height: calc(100vh - 80px);
    max-height: calc(100dvh - 80px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  }
}

/* ═══ MOBILE BOTTOM NAVIGATION ════════════════════════════════
   Fixed tab bar with the four most-used pages — Dashboard, Batches,
   Lab, Calendar. Hidden on desktop; shown on phones via the media
   query below. Sidebar remains accessible via the hamburger for the
   less-frequent pages (Inventory, Zones, Print, Strains). */
.bottom-nav {
  display: none;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: var(--c-surface);
  border-top: 1px solid var(--c-border);
  padding-bottom: env(safe-area-inset-bottom, 0);
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
}
.bottom-nav-btn {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-height: 56px;
  /* 2 px waagerecht und nicht 4, weil die fünf deutschen Beschriftungen bei
     320 px genau daran scheiterten. Gemessen, nicht geschätzt: Die Texte
     „Arbeitsgänge, Dashboard, Chargen, Labor, Kalender" sind 83 + 67 + 52 +
     36 + 55 = 293 px breit, dazu 5×8 px Polsterung macht 333 — und „Kalender"
     hing 14 px über den rechten Rand.

     Kein `min-width: 0`, und das ist der Punkt, an dem der naheliegende Griff
     falsch ist: `flex: 1` hat `flex-basis: 0`, also bekämen mit `min-width: 0`
     ALLE fünf Knöpfe dieselben 64 px, und „Arbeitsgänge" (83 px) würde
     abgeschnitten, obwohl die Summe passt. Ohne ihn nimmt jeder Knopf seine
     Inhaltsbreite, und 293 + 5×4 = 313 passen in 320.

     Der Rest ist sieben Pixel Luft, und die sind dünn. Sie sind aber gemessen
     und bleiben gemessen: Eine längere Übersetzung meldet der Lauf als
     OVERFLOW-Befund, statt sie stillschweigend abzuschneiden. */
  padding: 8px 2px;
  background: none;
  border: none;
  color: var(--c-text-muted);
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition:
    color 0.15s,
    background 0.15s;
}
.bottom-nav-btn:active {
  background: rgba(0, 0, 0, 0.05);
}
.bottom-nav-btn.active {
  color: var(--c-primary);
}
.bottom-nav-btn svg {
  flex-shrink: 0;
}

@media (max-width: 768px) {
  .bottom-nav {
    display: flex;
  }
  /* Reserve space so page content isn't hidden behind the fixed bar.
     56px nav + safe-area inset for devices with home indicators. */
  .content-wrap {
    padding-bottom: calc(56px + env(safe-area-inset-bottom, 0));
  }

  /* ── The touch floor, once, for everything that takes a tap ──
     Every other touch rule in this file names a component: .btn-sm and
     .btn-xs get one under (pointer: coarse), .sb-btn another, .sle-undo
     a third. That works exactly as far as the list someone remembered,
     and rendering the app against a seeded backend at 375px found five
     shapes that were never on it — the column-sort buttons in the batch
     table at 21px, the "Prüfen" buttons on the zone sections at 28px,
     the collapsed-group toggles at 27px, and "Wochenrhythmus bearbeiten"
     on the dashboard at 15px. All five carry one of the .fs-* utilities
     and nothing else, which is what made them look styled.

     So this stops enumerating components. 347 buttons render on a phone;
     this moves 12 of them, and every one it moves was under the floor or
     (three calendar buttons) at the universal 44px minimum rather than
     the 48px §9 chose for Büro work. Nothing already above it changes.

     A <div> with an onclick is a control the markup does not declare, so
     no selector can find it in general. .location-section-header is the
     one this app has. */
  button,
  [role='button'],
  .location-section-header {
    min-height: var(--tap-sm-min);
  }
  /* min-height alone makes a taller box with the label at the top of it
     when the button sets its own display; the UA's centring only applies
     to the default one. */
  button,
  [role='button'] {
    align-content: center;
  }
}

/* Die Benutzertabelle unter System wird in app.js gebaut, und ihre Zellen
   trugen ihre Polsterung INLINE. Inline schlägt jede Regel im Blatt, auch die
   92px Rinne des Kartenmodus: Beschriftung und Wert lagen übereinander,
   „ROLE" quer durch „admin". Gemessen hat das nichts, weil nichts überlief und
   nichts zu klein war — es stand nur falsch, und gesehen hat es erst ein Bild.
   Darum steht die Polsterung hier, mit einer Spezifität (0,1,1), die der
   Kartenmodus (0,3,3) überbieten kann. */
.us-tab th,
.us-tab td {
  padding: 6px;
}
.us-tab th {
  text-align: left;
  border-bottom: 1px solid var(--c-border);
}
/* Die Spalten legen sich an ihren Inhalt. `table { width: 100% }` verteilt den
   Rest gleichmäßig über die Spalten, und bei einer Zeile stand „probe" dadurch
   in einer Spalte von 254 px. Im Kartenmodus gewinnt `.t-cards { width: 100% }`
   weiter, weil es weiter unten im Blatt steht. */
.us-tab {
  width: auto;
}
/* ⚠️ **Ein Ankreuzfeld ist kein Textfeld** — und für diese zwei Spalten auch
   nicht die richtige Form. Die globale Regel (styles.css:2095) gibt jedem
   `input` `width: 100%`, `padding: 9px 12px` und `min-height: 64px`, und ein
   Ankreuzfeld erbt jedes Wort davon: hier 202 x 64 px um ein Häkchen von 13 px.
   Siehe Befund T8, der auch sagt, warum kein Messlauf das je gemeldet hat: Die
   TOUCH-Prüfung sucht Flächen, die zu KLEIN sind, zu groß ist keine Kategorie.

   Ein Ankreuzfeld auf Fingergröße zu ziehen behebt das nicht, es macht einen
   größeren Klotz: `appearance: auto` malt über den Polsterkasten, also ist die
   Trefferfläche IMMER genau so groß wie das gezeichnete Kästchen. Wer 44 px
   treffen will, sieht 44 px. Gemessen, nicht vermutet.

   Julian hat sich am 25.08. den Schalter ausgesucht. Er ist weiterhin ein
   echtes `input[type=checkbox]` — Tastatur, `:checked`, der bestehende
   Klick-Auswerter in app.js bleiben, wie sie sind — nur gezeichnet wird er
   selbst. Und selbst gezeichnet lassen sich Fläche und Bild trennen, was ein
   natives Kästchen nicht kann:

   Der Kasten ist `max(28px, --tap-sm-min)` hoch, also 48 px in der Hand und
   28 px am Schreibtisch (Regel R1: die Eingabeachse entscheidet, nicht die
   Breite). Die Pille darin ist immer 28 px hoch. Den Unterschied tragen
   durchsichtige Ränder, und `background-clip: padding-box` sorgt dafür, dass
   die Farbe nur im Polsterkasten liegt. Die Fläche hält damit die 44 px des
   Fingerbodens (WCAG 2.5.5) und die 24 px der Maus (2.5.8, Stufe AA), ohne
   dass das Bild mitwächst.

   Der Knopf ist ein zweiter Hintergrund und keine `::before`. Ein `input` ist
   ein ersetztes Element; dass Chrome dort Pseudoelemente zeichnet, sobald
   `appearance: none` gesetzt ist, gilt für WebKit nicht verlässlich, und
   Julian arbeitet in Safari. Ein Farbverlauf ist überall ein Hintergrund. Er
   wandert über `background-position`, das lässt sich weich schalten. */
.us-tab input[type='checkbox'] {
  appearance: none;
  -webkit-appearance: none;
  box-sizing: border-box;
  width: 50px;
  height: max(28px, var(--tap-sm-min));
  min-height: 0;
  padding: 0;
  margin: 0;
  border: 0 solid transparent;
  border-top-width: calc((max(28px, var(--tap-sm-min)) - 28px) / 2);
  border-bottom-width: calc((max(28px, var(--tap-sm-min)) - 28px) / 2);
  border-radius: 999px;
  background-color: var(--c-border);
  background-image: radial-gradient(circle 11px at 14px 50%, #fff 98%, rgba(255, 255, 255, 0) 100%);
  background-repeat: no-repeat;
  background-clip: padding-box;
  background-position: 0 0;
  cursor: pointer;
}
.us-tab input[type='checkbox']:checked {
  background-color: var(--c-primary);
  background-position: 22px 0;
}
/* Ein Schalter, der ohne Bewegung umspringt, sagt nicht, dass er geschaltet
   hat. Mit Bewegungsempfindlichkeit springt er trotzdem, das ist die Regel. */
@media (prefers-reduced-motion: no-preference) {
  .us-tab input[type='checkbox'] {
    transition:
      background-color 0.15s,
      background-position 0.15s;
  }
}
/* Admins dürfen immer, das Feld ist gesperrt. Gesperrt heißt hier gedämpft und
   nicht ausgegraut: der Zustand ist WAHR und soll ablesbar bleiben. */
.us-tab input[type='checkbox']:disabled {
  opacity: 0.5;
  cursor: default;
}
.us-tab input[type='checkbox']:focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 2px;
}

/* zwilling: kartenmodus */
@media (max-width: 768px) {
  /* ── Tables as cards ────────────────────────────────────────
     A 12-column table on a 360px viewport puts its Actions column
     roughly 700px off-screen. Below this breakpoint the rows become
     stacked cards: thead off, tr a card, td a label/value row, and the
     label read from the data-mlabel the renderer already writes.

     One mechanism, applied by class. Four tables had this spelled out
     separately — #t-batches, #t-cultures, #t-log, #t-harvest — in four
     blocks that were the same rules under different ids, 306 lines of
     it. The cost of that shape was never the duplication: #t-grain is
     filled by batchRowHtml(), the *same function* that fills
     #t-batches, so every one of its cells already carried a
     data-mlabel — and it still got a sideways-scrolling table, because
     the fifth copy of the block was never written.

     Two structural facts now do the work four ids used to:
       - the card header is the first cell, in all four tables and by
         construction — batch id, culture id, log time, harvest date;
       - the actions cell is the one with no data-mlabel, because a row
         of buttons has nothing to label. That is what distinguishes it
         from a last cell that *is* data, which is what #t-harvest has
         and why :last-child would have been wrong. */
  .t-cards {
    border: 0;
    width: 100%;
  }
  .t-cards > thead {
    display: none;
  }
  .t-cards > tbody > tr {
    display: block;
    border: 1px solid var(--c-border);
    border-radius: 10px;
    background: var(--c-surface);
    margin-bottom: 10px;
    padding: 12px 14px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
  }
  .t-cards > tbody > tr > td {
    display: block;
    padding: 4px 0;
    border: 0;
    font-size: 0.8125rem;
    line-height: 1.4;
    /* The card carries the background now, not the cell. Two table rules
       still target these cells — `tr:nth-child(even) td` and `tr:hover td`
       — and both outlive the layout they were written for: zebra striping
       tints every second *card* grey instead of every second row, and a
       tap on a coarse pointer leaves the hover tint stuck on. Neither was
       introduced by the card mode; neither had been looked at since it
       arrived. */
    background: none;
  }
  /* Label in a gutter, value in normal flow.

     This cell was `display: flex` with the label as its first item, which
     reads correctly and is wrong: making the cell a flex container makes
     *every* element child a flex item, so the layout silently depended on
     each cell holding exactly one. #t-batches' Substrat cell holds three —
     batchRowHtml() writes one .sub-tag per material — and they became three
     competing items on a nowrap line, each shrunk until "HW 60% WB 20%"
     wrapped to four lines inside a 99px-radius pill. Eight cards, every
     batch, and no count could see it: the chips do not exist until the
     renderer runs, so both the source census and measure-mobile.js (which
     serves the page with the scripts stripped) looked straight past them.

     Normal flow is also what these chips get from the desktop table cell
     they were written for, so the two layouts now differ in where the label
     sits and not in how the value is laid out. */
  .t-cards > tbody > tr > td[data-mlabel]:not(:first-child) {
    position: relative;
    padding-left: 92px;
  }
  /* The label was 10–11px here, which is the size this whole project
     exists to stop shipping. Uppercase, 600 and muted already say
     "this is a label" without help from a size below the floor.
     84px fits every label the renderers emit except "Chargen-ID" at 87px,
     which is a first cell and hidden; the rest top out at 72px. */
  .t-cards > tbody > tr > td[data-mlabel]:not(:first-child)::before {
    content: attr(data-mlabel);
    position: absolute;
    left: 0;
    top: 4px;
    width: 84px;
    color: var(--c-text-muted);
    font-size: var(--fs-xs);
    font-weight: 600;
    line-height: 1.4;
    text-transform: uppercase;
    letter-spacing: 0.02em;
  }
  /* The other half of the same cell. Normal flow fixed the three chips
     competing as flex items; it does not fix each chip being an inline box,
     which splits its background and its 99px radius across two line boxes the
     moment it wraps — "RH 63%" came out as a 155×39 smear. A pill has to be a
     box. Scoped to the card because the desktop table's Substrat column is
     ~63px wide, where the same keyword wraps every chip into a padded stack
     and takes #t-batches from 760px to 1093px tall. Same defect, different
     fix: the desktop needs its column widths looked at. */
  .t-cards .sub-tag {
    display: inline-block;
  }
  /* First cell: the card's header. What it looks like is per table — an
     id wants to be big, a timestamp wants to be a quiet subtitle — so
     this sets only what they share. Its label is not hidden here but never
     written: the :not(:first-child) above is what keeps 184 of the 189
     first cells from indenting 92px around a label nobody wants to see. */
  .t-cards > tbody > tr > td:first-child {
    padding-bottom: 6px;
    margin-bottom: 6px;
    border-bottom: 1px solid var(--c-border-light);
  }
  /* Actions row — full width, buttons share it equally. The
     (pointer: coarse) rule near .btn-sm already gives them 44px. */
  .t-cards > tbody > tr > td:not([data-mlabel]):not(:first-child):not(.empty) {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--c-border-light);
    white-space: normal !important;
  }
  .t-cards > tbody > tr > td:not([data-mlabel]):not(:first-child):empty {
    display: none;
  }
  .t-cards > tbody > tr > td:not([data-mlabel]) .btn-sm,
  .t-cards > tbody > tr > td:not([data-mlabel]) .btn-xs {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0 !important;
    text-align: center;
  }
  /* Empty/no-matches row keeps a centred full-row look. */
  .t-cards > tbody > tr > td.empty {
    display: block;
    text-align: center;
    padding: 18px 0;
    border-bottom: 0;
    margin-bottom: 0;
  }

  /* ── Per table, only what is genuinely its own ───────────────── */

  /* Batches and grain lead with an id, which is the card's name. */
  #t-batches > tbody > tr > td.bt-id,
  #t-grain > tbody > tr > td.bt-id {
    font-size: 0.9375rem !important;
    font-weight: 600;
  }
  /* The scan log and the harvest log lead with a time, which is not a
     name — it dates the row rather than titling it. */
  #t-log > tbody > tr > td.lg-time,
  #t-harvest > tbody > tr > td.hv-date {
    font-size: max(0.75rem, var(--fs-min)) !important;
    color: var(--c-text-sec) !important;
    padding-bottom: 4px;
    margin-bottom: 4px;
  }
  /* Sub-row injected by toggleBatchBags() — keep its full-width layout
     for the bag chips, just retint it as a connected detail panel. */
  #t-batches > tbody > tr[id^='brow-'],
  #t-grain > tbody > tr[id^='brow-'] {
    border: 1px dashed var(--c-border);
    padding: 10px 12px;
    background: var(--c-bg);
  }
  /* Harvest cards: grams pinned top-right as a big amber number, the
     most-scanned value when reading a harvest log on a phone. */
  #t-harvest > tbody > tr {
    position: relative;
  }
  #t-harvest > tbody > tr > td.hv-date {
    padding-right: 96px;
  }
  #t-harvest > tbody > tr > td.hv-grams {
    position: absolute;
    top: 8px;
    right: 12px;
    font-size: 1.25rem !important;
    font-weight: 600 !important;
    line-height: 1.1;
    padding: 0 !important;
  }
  #t-harvest > tbody > tr > td.hv-grams::before {
    display: none;
  }
}

/* zwilling: kartenmodus
   ── Regel R2: der Kasten misst, nicht das Fenster ───────────────────────
   Der Kartenmodus hing an der Fensterbreite, und darin steckte die Klippe bei
   769 px: dort kommt die Seitenleiste zurück und nimmt 240 px vom Inhalt weg,
   die Tabelle bekommt aber weiter das Layout für 769 px statt für die 497 px,
   die ihr geblieben sind. Gemessen war das der Grund für zwei Ratschenzeilen
   (Labor 769-860 px verkleinert, Kontaminationsstreifen 769-800 px über den
   Rand) — ein Fehler mit zwei Gesichtern, je nachdem ob die Meta-Ansicht gilt.

   736 px ist keine neue Grenze, sondern dieselbe: Auf dem Telefon ist die
   Seitenleiste weg und `.main` hat 2×16 px Polsterung, 768 − 32 = 736. Der
   Kartenmodus setzt also auf dem Telefon exakt dort ein wie vorher, und auf
   dem Schreibtisch zusätzlich, sobald die Inhaltsspalte schmal genug ist —
   was auch heißt: Wird die Seitenleiste eingeklappt, folgt er von selbst.
   Keine Medienabfrage der Welt kann das.

   ZWEIMAL GESCHRIEBEN, mit Absicht. Die @media-Fassung ist die Rückfallebene
   für einen Browser ohne Container-Abfragen (Chrome vor 105, Safari vor 16);
   dort bleibt alles, wie es heute ist. Ein Test hält die beiden Fassungen
   Zeichen für Zeichen gleich, weil zwei Kopien sonst genau so lange
   übereinstimmen, bis jemand eine davon ändert. */
@container inhalt (max-width: 736px) {
  /* ── Tables as cards ────────────────────────────────────────
     A 12-column table on a 360px viewport puts its Actions column
     roughly 700px off-screen. Below this breakpoint the rows become
     stacked cards: thead off, tr a card, td a label/value row, and the
     label read from the data-mlabel the renderer already writes.

     One mechanism, applied by class. Four tables had this spelled out
     separately — #t-batches, #t-cultures, #t-log, #t-harvest — in four
     blocks that were the same rules under different ids, 306 lines of
     it. The cost of that shape was never the duplication: #t-grain is
     filled by batchRowHtml(), the *same function* that fills
     #t-batches, so every one of its cells already carried a
     data-mlabel — and it still got a sideways-scrolling table, because
     the fifth copy of the block was never written.

     Two structural facts now do the work four ids used to:
       - the card header is the first cell, in all four tables and by
         construction — batch id, culture id, log time, harvest date;
       - the actions cell is the one with no data-mlabel, because a row
         of buttons has nothing to label. That is what distinguishes it
         from a last cell that *is* data, which is what #t-harvest has
         and why :last-child would have been wrong. */
  .t-cards {
    border: 0;
    width: 100%;
  }
  .t-cards > thead {
    display: none;
  }
  .t-cards > tbody > tr {
    display: block;
    border: 1px solid var(--c-border);
    border-radius: 10px;
    background: var(--c-surface);
    margin-bottom: 10px;
    padding: 12px 14px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
  }
  .t-cards > tbody > tr > td {
    display: block;
    padding: 4px 0;
    border: 0;
    font-size: 0.8125rem;
    line-height: 1.4;
    /* The card carries the background now, not the cell. Two table rules
       still target these cells — `tr:nth-child(even) td` and `tr:hover td`
       — and both outlive the layout they were written for: zebra striping
       tints every second *card* grey instead of every second row, and a
       tap on a coarse pointer leaves the hover tint stuck on. Neither was
       introduced by the card mode; neither had been looked at since it
       arrived. */
    background: none;
  }
  /* Label in a gutter, value in normal flow.

     This cell was `display: flex` with the label as its first item, which
     reads correctly and is wrong: making the cell a flex container makes
     *every* element child a flex item, so the layout silently depended on
     each cell holding exactly one. #t-batches' Substrat cell holds three —
     batchRowHtml() writes one .sub-tag per material — and they became three
     competing items on a nowrap line, each shrunk until "HW 60% WB 20%"
     wrapped to four lines inside a 99px-radius pill. Eight cards, every
     batch, and no count could see it: the chips do not exist until the
     renderer runs, so both the source census and measure-mobile.js (which
     serves the page with the scripts stripped) looked straight past them.

     Normal flow is also what these chips get from the desktop table cell
     they were written for, so the two layouts now differ in where the label
     sits and not in how the value is laid out. */
  .t-cards > tbody > tr > td[data-mlabel]:not(:first-child) {
    position: relative;
    padding-left: 92px;
  }
  /* The label was 10–11px here, which is the size this whole project
     exists to stop shipping. Uppercase, 600 and muted already say
     "this is a label" without help from a size below the floor.
     84px fits every label the renderers emit except "Chargen-ID" at 87px,
     which is a first cell and hidden; the rest top out at 72px. */
  .t-cards > tbody > tr > td[data-mlabel]:not(:first-child)::before {
    content: attr(data-mlabel);
    position: absolute;
    left: 0;
    top: 4px;
    width: 84px;
    color: var(--c-text-muted);
    font-size: var(--fs-xs);
    font-weight: 600;
    line-height: 1.4;
    text-transform: uppercase;
    letter-spacing: 0.02em;
  }
  /* The other half of the same cell. Normal flow fixed the three chips
     competing as flex items; it does not fix each chip being an inline box,
     which splits its background and its 99px radius across two line boxes the
     moment it wraps — "RH 63%" came out as a 155×39 smear. A pill has to be a
     box. Scoped to the card because the desktop table's Substrat column is
     ~63px wide, where the same keyword wraps every chip into a padded stack
     and takes #t-batches from 760px to 1093px tall. Same defect, different
     fix: the desktop needs its column widths looked at. */
  .t-cards .sub-tag {
    display: inline-block;
  }
  /* First cell: the card's header. What it looks like is per table — an
     id wants to be big, a timestamp wants to be a quiet subtitle — so
     this sets only what they share. Its label is not hidden here but never
     written: the :not(:first-child) above is what keeps 184 of the 189
     first cells from indenting 92px around a label nobody wants to see. */
  .t-cards > tbody > tr > td:first-child {
    padding-bottom: 6px;
    margin-bottom: 6px;
    border-bottom: 1px solid var(--c-border-light);
  }
  /* Actions row — full width, buttons share it equally. The
     (pointer: coarse) rule near .btn-sm already gives them 44px. */
  .t-cards > tbody > tr > td:not([data-mlabel]):not(:first-child):not(.empty) {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--c-border-light);
    white-space: normal !important;
  }
  .t-cards > tbody > tr > td:not([data-mlabel]):not(:first-child):empty {
    display: none;
  }
  .t-cards > tbody > tr > td:not([data-mlabel]) .btn-sm,
  .t-cards > tbody > tr > td:not([data-mlabel]) .btn-xs {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0 !important;
    text-align: center;
  }
  /* Empty/no-matches row keeps a centred full-row look. */
  .t-cards > tbody > tr > td.empty {
    display: block;
    text-align: center;
    padding: 18px 0;
    border-bottom: 0;
    margin-bottom: 0;
  }

  /* ── Per table, only what is genuinely its own ───────────────── */

  /* Batches and grain lead with an id, which is the card's name. */
  #t-batches > tbody > tr > td.bt-id,
  #t-grain > tbody > tr > td.bt-id {
    font-size: 0.9375rem !important;
    font-weight: 600;
  }
  /* The scan log and the harvest log lead with a time, which is not a
     name — it dates the row rather than titling it. */
  #t-log > tbody > tr > td.lg-time,
  #t-harvest > tbody > tr > td.hv-date {
    font-size: max(0.75rem, var(--fs-min)) !important;
    color: var(--c-text-sec) !important;
    padding-bottom: 4px;
    margin-bottom: 4px;
  }
  /* Sub-row injected by toggleBatchBags() — keep its full-width layout
     for the bag chips, just retint it as a connected detail panel. */
  #t-batches > tbody > tr[id^='brow-'],
  #t-grain > tbody > tr[id^='brow-'] {
    border: 1px dashed var(--c-border);
    padding: 10px 12px;
    background: var(--c-bg);
  }
  /* Harvest cards: grams pinned top-right as a big amber number, the
     most-scanned value when reading a harvest log on a phone. */
  #t-harvest > tbody > tr {
    position: relative;
  }
  #t-harvest > tbody > tr > td.hv-date {
    padding-right: 96px;
  }
  #t-harvest > tbody > tr > td.hv-grams {
    position: absolute;
    top: 8px;
    right: 12px;
    font-size: 1.25rem !important;
    font-weight: 600 !important;
    line-height: 1.1;
    padding: 0 !important;
  }
  #t-harvest > tbody > tr > td.hv-grams::before {
    display: none;
  }
}

/* Clearance for the floating scan button. It is gated on the pointer, not on
   width, so the 56 px reserved above for the bottom-nav misses it twice: on a
   phone it reaches higher than the bar, and on a coarse-pointer tablet past
   768 px there is no bar and no reserve at all. It sits --fab-bottom (72 px) up
   and stands 56 px tall, so its top edge is 128 px above the bottom. Without
   this the last stretch of any long page hides underneath;
   Admin → Kanäle put its "Jetzt synchronisieren" button there.
   Must stay after the bottom-nav rule: same specificity, later wins. */
@media (pointer: coarse) and (hover: none) {
  .content-wrap {
    padding-bottom: calc(140px + env(safe-area-inset-bottom, 0));
  }
}

/* ═══ CONTAMINATION REPORT MODAL ═══════════════════════════════
   Capture flow: type chips, severity row, 80x80 photo tiles. Photos
   are compressed to ~200KB JPEG client-side before submit (canvas
   resize at 1280px / quality 0.8). */
/* Foto und Notiz: das, was am Regal manchmal dazukommt. Eng gesetzt, damit
   Senden am Telefon ohne Scrollen zu sehen ist. */
.cr-quick {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 10px;
}
.cr-quick .contam-photo-tile {
  width: 64px;
  height: 64px;
}
.cr-quick .contam-photo-tile.add {
  width: auto;
  padding: 0 14px;
  font-size: max(0.875rem, var(--fs-min));
  font-weight: 600;
}
.cr-quick #cr-photo-status:empty {
  display: none;
}
/* Typ, Schwere, Verschieben: zugeklappt. Die Zeile ist eine Daumenbreite hoch,
   damit sie sich am Telefon treffen lässt. */
.cr-more summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  font-size: max(0.8125rem, var(--fs-min));
  font-weight: 600;
  color: var(--c-text-sec);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  list-style: none;
  gap: 8px;
}
.cr-more summary::-webkit-details-marker {
  display: none;
}
/* Der Pfeil sagt, dass die Zeile aufgeht; display:flex nimmt den eigenen weg. */
.cr-more summary::before {
  content: '';
  width: 7px;
  height: 7px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.15s;
  flex-shrink: 0;
}
.cr-more[open] summary::before {
  transform: rotate(45deg);
}
.cr-more-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 4px;
}
.cr-more-body .contam-severity-row {
  margin-bottom: 6px;
}
.cr-auto-move-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: max(0.8125rem, var(--fs-min));
  color: var(--c-text-sec);
  cursor: pointer;
}
.cr-auto-move-row input {
  width: 24px;
  height: 24px;
  cursor: pointer;
}
.cr-label {
  display: block;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  color: var(--c-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 6px;
}
/* Die Beutel, über die der Bericht geht — einer je Plättchen, mit einem × zum
   Streichen, dahinter der Knopf für weitere. Am Telefon bricht die Zeile um,
   statt zu scrollen: eine Reihe können acht Beutel sein. */
.cr-target-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
.cr-target-chip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 4px 4px 4px 8px;
  border-radius: 6px;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  color: var(--c-text);
  font-size: 0.8125rem;
}
.cr-target-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Dasselbe Maß wie jeder andere Knopf im Formular: das × steht am Regal
     zwischen zwei Beuteln und wird mit dem Daumen getroffen, nicht mit der
     Maus. */
  width: 44px;
  min-height: 44px;
  border: none;
  border-radius: 5px;
  background: none;
  color: var(--c-text-muted);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.cr-target-x:active {
  background: rgba(0, 0, 0, 0.08);
  color: var(--c-text);
}
.cr-target-add {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 4px 12px;
  border: 1px dashed var(--c-border);
  border-radius: 6px;
  background: none;
  color: var(--c-text-sec);
  font-family: inherit;
  font-size: 0.8125rem;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.cr-target-add:active {
  background: rgba(0, 0, 0, 0.05);
}
/* Ohne gesammelte Beutel steht der Knopf allein hinter dem Text der Charge. */
.cr-target-text + .cr-target-add {
  margin-left: 8px;
}
/* Melden-Knopf in der Scan-Leiste: rot, weil er das Formular über bereits
   markierte Beutel öffnet, und das ist der Abschluss des Sammelns. */
.btn-contam-collect {
  background: #dc2626;
  color: #fff;
  border-color: #dc2626;
}
.contam-type-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 6px;
}
.contam-type-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 12px;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  background: var(--c-surface);
  font-size: 0.8125rem;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  min-height: 44px;
  -webkit-tap-highlight-color: transparent;
  transition:
    border-color 0.15s,
    background 0.15s;
}
.contam-type-btn:active {
  background: rgba(0, 0, 0, 0.05);
}
.contam-type-btn.active {
  border-color: currentColor;
  background: rgba(0, 0, 0, 0.04);
}
.contam-type-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  flex-shrink: 0;
}
.contam-severity-row {
  display: flex;
  gap: 6px;
}
.contam-sev-btn {
  flex: 1;
  min-height: 44px;
  padding: 10px 12px;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  background: var(--c-surface);
  font-family: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: all 0.15s;
}
.contam-sev-btn:active {
  background: rgba(0, 0, 0, 0.05);
}
.contam-sev-btn.active {
  background: var(--c-primary);
  color: #fff;
  border-color: var(--c-primary);
}
.contam-sev-btn[data-sev='major'].active {
  background: #f59e0b;
  border-color: #f59e0b;
}
.contam-sev-btn[data-sev='lost'].active {
  background: #dc2626;
  border-color: #dc2626;
}
.contam-photo-tiles {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.contam-photo-tile {
  width: 80px;
  height: 80px;
  border-radius: 8px;
  border: 1px solid var(--c-border);
  background: var(--c-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
  cursor: pointer;
  overflow: hidden;
  position: relative;
  -webkit-tap-highlight-color: transparent;
  flex-shrink: 0;
}
.contam-photo-tile.add {
  border-style: dashed;
  font-size: 1.375rem;
}
.contam-photo-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.contam-photo-tile .remove {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  border: none;
  font-size: 0.875rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Annotation hint chip — small "edit" pencil overlay on the bottom-left of
   each photo tile so workers know they can tap to draw on it. */
.contam-photo-tile .annot-hint {
  position: absolute;
  bottom: 2px;
  left: 2px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  font-size: max(0.75rem, var(--fs-min));
}

/* ═══ PHOTO ANNOTATION MODAL ══════════════════════════════════
   Full-screen-ish drawing surface. The canvas size matches the photo's
   intrinsic dimensions so strokes composite at the same pixel scale on
   submit. Toolbar header with Clear / Cancel / Done. */
.pa-shell {
  background: var(--c-surface);
  width: 96%;
  max-width: 720px;
  height: 92vh;
  height: 92dvh;
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.3);
}
.pa-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--c-border-light);
  flex-wrap: wrap;
}
.pa-title {
  flex: 1;
  font-size: 0.875rem;
  font-weight: 600;
  min-width: 100px;
}
.pa-canvas-wrap {
  flex: 1;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  /* Allow vertical pan (some content may overflow on tiny landscape) but
     not horizontal/zoom — drawing gestures are absorbed by the canvas. */
  touch-action: pan-y;
}
#pa-canvas {
  max-width: 100%;
  max-height: 100%;
  display: block;
  /* Stroke gestures, not scroll. */
  touch-action: none;
}
.pa-hint {
  padding: 8px 12px;
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
  text-align: center;
  border-top: 1px solid var(--c-border-light);
}

/* ═══ CONTAMINATION REPORT LIST + DRILL-DOWN ═══════════════════
   The Lab → Kontaminationen sub-tab. Card-per-report layout works
   on both phone and desktop without a JS branch. */
.contam-list-card {
  display: flex;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--c-border);
  border-radius: 10px;
  background: var(--c-surface);
  margin-bottom: 8px;
  cursor: pointer;
  transition: background 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.contam-list-card:hover {
  background: rgba(0, 0, 0, 0.02);
}
.contam-list-card:active {
  background: rgba(0, 0, 0, 0.05);
}
.contam-list-thumb {
  width: 64px;
  height: 64px;
  flex-shrink: 0;
  border-radius: 6px;
  background: var(--c-bg);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--c-text-muted);
  font-size: 1.125rem;
}
.contam-list-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.contam-list-meta {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.contam-list-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}
.contam-list-bag {
  font-family: monospace;
  font-size: 0.8125rem;
  font-weight: 600;
}
.contam-list-batch {
  font-family: monospace;
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
}
.contam-list-when {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
  margin-left: auto;
}
.contam-type-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  background: rgba(0, 0, 0, 0.04);
}
.contam-type-badge .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}
.contam-sev-badge {
  font-size: max(0.625rem, var(--fs-min));
  font-weight: 600;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 4px;
  letter-spacing: 0.04em;
}
.contam-sev-badge.sev-minor {
  background: #f1f5f9;
  color: var(--c-text-sec);
}
.contam-sev-badge.sev-major {
  background: #fef3c7;
  color: #92400e;
}
.contam-sev-badge.sev-lost {
  background: #fee2e2;
  color: #991b1b;
}
.contam-resolved-badge {
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  background: #dcfce7;
  color: #14532d;
}
.contam-open-badge {
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  background: #fef3c7;
  color: #92400e;
}
/* Resolved cards in the browse list — fade slightly so the eye lands on
   open ones first; still tappable, still readable. */
.contam-list-card.is-resolved {
  opacity: 0.65;
  background: var(--c-bg);
}
.contam-list-card.is-resolved:hover {
  opacity: 0.9;
  background: rgba(0, 0, 0, 0.02);
}
/* Resolve action row in the drill-down modal */
.cd-resolve-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--c-border-light);
}
.contam-list-notes {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-sec);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Drill-down modal: photo gallery + metadata grid */
.cd-meta-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 16px;
  margin-bottom: 14px;
  font-size: 0.8125rem;
}
.cd-meta-grid .label {
  color: var(--c-text-muted);
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.cd-photos {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  margin: 0 -20px 14px;
  padding: 0 20px 4px;
  -webkit-overflow-scrolling: touch;
}
.cd-photo {
  flex: 0 0 auto;
  width: 240px;
  height: 240px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--c-bg);
  scroll-snap-align: start;
  cursor: zoom-in;
}
.cd-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.cd-notes {
  padding: 10px 12px;
  background: var(--c-bg);
  border-radius: 6px;
  font-size: 0.8125rem;
  line-height: 1.5;
  white-space: pre-wrap;
}

@media (max-width: 480px) {
  .cd-meta-grid {
    grid-template-columns: 1fr;
  }
  .cd-photo {
    width: 80vw;
    height: 80vw;
    max-width: 320px;
    max-height: 320px;
  }
}

/* ═══ ARBEITSGÄNGE ══════════════════════════════════════════════════════════
   The launcher page and the shared flow modal. Tiles are deliberately large:
   this is the screen a new worker meets first, and on the tablet it is used
   standing up. Line icons, not emoji — same visual language as the sidebar. */

/* Wieder ganz oben, also ohne Abstand nach oben: die Kacheln stehen seit dem
   11.09.2026 über der Tagesliste, damit das Telefon auf ihnen öffnet. Den
   Abstand zwischen den beiden Hälften trägt jetzt .dash-top-row. */
.wk-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 4px;
}
.wk-sub {
  font-size: var(--fs-sm);
  color: var(--c-text-muted);
  margin-top: 3px;
}
/* Aufs Handy legen: dieselbe Bauart wie der Hinweis oben, aber ruhiger — das
   ist ein Angebot, keine Aufgabe. Die eigene [hidden]-Regel ist nötig, weil
   display:flex eine Autoren-Regel ist und sonst die des Browsers schlägt;
   derselbe Fehler stand bis vor Kurzem auf dem Zurück-Knopf. */
.wk-install {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  background: var(--c-blue-light);
  border: 1px solid var(--c-blue-border);
  color: var(--c-blue-dark);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  font-size: var(--fs-sm);
  font-weight: 600;
  margin: 14px 0 0;
}
.wk-install[hidden] {
  display: none;
}
.wk-install .btn {
  margin-left: auto;
  min-height: var(--tap);
  border-color: var(--c-blue-dark);
  color: var(--c-blue-dark);
}

.wk-tiles {
  display: grid;
  /* Fixed four across, so the launcher reads as four things that make
     something and three that are done to it — an auto-fill grid put six on a
     wide screen and left the seventh stranded on its own line. The second row
     keeps the same four-column track, so every tile is the same width. */
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
/* Wo steht was: one chip per zone, and under the chosen one a tap-to-unfold row
   per species and move-in day. The date line is the reason the card exists, so
   it is set in the secondary text colour rather than muted. */
.wk-tents {
  margin-top: 12px;
  padding: 12px 16px;
}
.wk-zchips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.wk-zchip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  max-width: 100%;
  padding: 6px 12px;
  border: 1px solid var(--c-border);
  border-radius: 22px;
  background: var(--c-surface);
  color: var(--c-text);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
}
.wk-zchip[aria-pressed='true'] {
  border-color: var(--c-accent);
  box-shadow: inset 0 0 0 1px var(--c-accent);
  font-weight: 600;
}
.wk-zchip .zone-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
}
.wk-zchip-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wk-zchip-n {
  color: var(--c-text-muted);
  font-weight: 600;
}
.wk-zone-rows {
  margin-top: 8px;
  border-top: 1px solid var(--c-border);
}
.wk-tent-row summary {
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: 8px;
  padding: 6px 0;
  min-height: 44px;
  align-content: center;
  cursor: pointer;
  list-style: none;
}
.wk-tent-row summary::-webkit-details-marker {
  display: none;
}
.wk-tent-row + .wk-tent-row summary {
  border-top: 1px dashed var(--c-border);
}
.wk-tent-sp {
  font-weight: 500;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wk-tent-n {
  font-weight: 600;
  white-space: nowrap;
}
.wk-tent-when {
  grid-column: 1 / -1;
  font-size: 0.875rem;
  color: var(--c-text-sec);
}
.wk-tent-detail {
  padding: 0 0 8px;
  font-size: 0.8125rem;
  color: var(--c-text-sec);
}
.wk-tent-batch {
  font-family: monospace;
  color: var(--c-text-muted);
}
.wk-tent-empty {
  padding: 10px 0 4px;
  font-size: 0.875rem;
  color: var(--c-text-muted);
}
/* Only there when something is wrong. Sized to its text, not stretched. */
.wk-alert {
  display: flex;
  width: fit-content;
  max-width: 100%;
  align-items: center;
  min-height: 44px;
  margin: 0 0 12px;
  padding: 8px 14px;
  border: 1px solid var(--c-amber-border);
  border-radius: var(--radius);
  background: var(--c-amber-light);
  color: var(--c-amber-dark);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.wk-alert[hidden] {
  display: none;
}
/* Nachproduzieren on the work page (renderWorkNachproduzieren): the alert's
   shape, red while something is urgent, amber while it is only short. */
.wk-alert.wk-np-now {
  border-color: var(--c-red-border);
  background: var(--c-red-light);
  color: var(--c-red-dark);
}
/* Today's USB stick backup, for admins: the alert's shape, coloured by what
   it asks for. Blue while a backup runs, amber when one is due or nothing is
   set up, green when the stick can come out, red when it failed. */
.wk-usb {
  display: flex;
  width: fit-content;
  max-width: 100%;
  align-items: center;
  min-height: 44px;
  margin: 0 0 12px;
  padding: 8px 14px;
  border: 1px solid var(--c-blue-border);
  border-radius: var(--radius);
  background: var(--c-blue-light);
  color: var(--c-blue-dark);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.wk-usb[hidden] {
  display: none;
}
.wk-usb-due,
.wk-usb-setup,
.wk-usb-key,
.wk-usb-gaps,
.wk-usb-first {
  border-color: var(--c-amber-border);
  background: var(--c-amber-light);
  color: var(--c-amber-dark);
}
.wk-usb-remove {
  border-color: var(--c-green-border);
  background: var(--c-green-light);
  color: var(--c-green-dark);
}
.wk-usb-error,
.wk-usb-state {
  border-color: var(--c-red-border);
  background: var(--c-red-light);
  color: var(--c-red-dark);
}
/* The task card's heading is the day in one line and the switch for the list. */
.wk-today {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.wk-today-sum {
  flex: 1;
  min-width: 0;
  font-size: 0.875rem;
  color: var(--c-text-sec);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wk-today-chev {
  color: var(--c-text-muted);
  transition: transform 0.15s;
}
.wk-today[aria-expanded='true'] .wk-today-chev {
  transform: rotate(180deg);
}
.wk-today[aria-expanded='true'] {
  margin-bottom: 8px;
}
#dash-batch-tasks-card.wk-tasks-closed #dash-batch-tasks {
  display: none;
}
/* Two columns from 900px down, phones included. One column put seven full-width
   tiles above everything else on the start screen. Two fit since the titles
   became one or two words ("Labor", "Körnerbrut") and the phone drops the
   when-line (below). */
@media (max-width: 900px) {
  .wk-tiles {
    grid-template-columns: repeat(2, 1fr);
  }
}
.wk-tile {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-rows: auto 1fr;
  align-content: start;
  gap: 7px;
  text-align: left;
  background: linear-gradient(150deg, var(--wk-cl) 0%, var(--c-surface) 62%);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: var(--pad-tile);
  min-height: 156px;
  cursor: pointer;
  font: inherit;
  color: var(--c-text);
  box-shadow: var(--shadow);
  transition:
    border-color 0.15s,
    box-shadow 0.15s,
    transform 0.1s;
}
.wk-tile:hover {
  border-color: var(--wk-c);
  box-shadow: var(--shadow-md);
}
.wk-tile:active {
  transform: scale(0.985);
}
/* The drawing sits behind the words and bleeds off the corner, so the tile reads
   as a picture of the job rather than a labelled box. It is drawn, not
   photographed, because this app runs offline in the lab: nothing to load, sharp
   at any size, a few hundred bytes each. Every shape takes the tile's own colour
   through currentColor, so the seven read as one family. */
/* A veil between the drawing and the words. The artwork may bleed as far across
   the tile as it likes; the text side stays calm regardless of what is drawn
   underneath, so legibility never depends on the shape of the picture. */
.wk-tile::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(102deg, var(--c-surface) 24%, rgba(255, 255, 255, 0) 72%);
  opacity: 0.85;
  pointer-events: none;
  z-index: 1;
}
.wk-tile-art {
  z-index: 0;
  position: absolute;
  right: -10px;
  bottom: -8px;
  width: 134px;
  color: var(--wk-c);
  pointer-events: none;
}
.wk-tile-art svg {
  width: 100%;
  height: auto;
  display: block;
}
.wk-tile-t {
  position: relative;
  z-index: 2;
  font-size: var(--fs-tile);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
  max-width: 90%;
}
.wk-tile-when {
  position: relative;
  z-index: 2;
  font-size: max(0.71875rem, var(--fs-min));
  color: var(--c-text-muted);
  line-height: 1.4;
  max-width: 64%;
}

/* The phone tile, after the four rules above rather than before them: a media
   query adds no specificity, so between two `.wk-tile` rules the later one wins
   and the base min-height would simply have stood. */
@media (max-width: 768px) {
  /* Two tiles side by side, each about 165px wide at 375px. There the
     when-line would wrap to four or five lines of 13px, so the phone shows
     the title alone. The drawing does not bleed here: at this size a
     corner of a picture reads as a smudge, so it stands whole on the right
     at one fixed size on every tile, and the title takes the rest. No veil
     is needed with nothing underneath the words. */
  .wk-tiles {
    gap: 10px;
  }
  .wk-tile {
    grid-template-columns: minmax(0, 1fr) 52px;
    grid-template-rows: auto;
    align-items: center;
    column-gap: 8px;
    min-height: 72px;
    padding: 10px 10px 10px 14px;
  }
  .wk-tile::after {
    display: none;
  }
  .wk-tile-art {
    position: static;
    grid-column: 2;
    grid-row: 1;
    width: 52px;
  }
  .wk-tile-t {
    grid-column: 1;
    grid-row: 1;
    max-width: none;
    overflow-wrap: anywhere;
  }
  .wk-tile-when {
    display: none;
  }
}

/* Reihenfolge ändern: a quiet link under the tiles, and a numbered list with
   up/down buttons that moves the tiles as you tap. */
.wk-order-bar {
  display: flex;
  justify-content: flex-end;
  margin-top: 6px;
}
.wk-order-link {
  min-height: 44px;
  padding: 0 4px;
  border: 0;
  background: none;
  color: var(--c-text-sec);
  font: inherit;
  font-size: 0.875rem;
  text-decoration: underline;
  cursor: pointer;
}
.wk-order {
  margin-top: 4px;
  padding: 10px 14px;
}
.wk-order[hidden] {
  display: none;
}
.wk-order-list {
  margin: 0;
  padding-left: 1.5em;
}
.wk-order-row {
  padding: 2px 0;
}
.wk-order-row > * {
  vertical-align: middle;
}
.wk-order-name {
  display: inline-block;
  min-width: 9em;
  margin-right: 8px;
}
.wk-order-row .btn {
  min-width: 44px;
  margin-left: 4px;
}
.wk-order-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 10px;
}

/* ── flow modal ─────────────────────────────────────────────────────────── */
.wkf-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.wkf-head h3 {
  margin: 0;
  flex: 1;
  font-size: 1.0625rem;
}
.wkf-chips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.wkf-chip {
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--c-border);
  color: var(--c-text-muted);
  background: var(--c-surface);
}
.wkf-chip.done {
  color: var(--c-green-dark);
  background: var(--c-primary-light);
  border-color: var(--c-green-border);
}
.wkf-chip.now {
  color: #fff;
  background: var(--c-primary);
  border-color: var(--c-primary);
}
.wkf-empty {
  font-size: 0.8125rem;
  color: var(--c-text-muted);
  padding: 22px 0;
  text-align: center;
}
.wkf-foot {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  border-top: 1px solid var(--c-border);
  margin-top: 14px;
  padding-top: 14px;
}

/* batch list */
.wkf-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* dvh, not vh: with the on-screen keyboard up, vh still measures the window
     as if it were not there, so a list sized in vh keeps its full height and
     pushes the footer buttons off the bottom. This list is step 1 of every
     flow and the field below it is what opens the keyboard. */
  max-height: 46dvh;
  overflow-y: auto;
}
.wkf-row {
  display: grid;
  grid-template-columns: minmax(120px, auto) 1fr auto;
  gap: 10px;
  align-items: center;
  text-align: left;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  cursor: pointer;
  font: inherit;
  color: var(--c-text);
  min-height: max(46px, var(--tap-min));
}
.wkf-row:hover {
  border-color: var(--c-primary);
  background: var(--c-primary-light);
}
.wkf-row-id {
  font-family: ui-monospace, 'Cascadia Code', monospace;
  font-size: max(0.78125rem, var(--fs-min));
  font-weight: 700;
}
.wkf-row-meta {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
}
.wkf-row-zone {
  font-size: max(0.6875rem, var(--fs-min));
  font-weight: 700;
  color: var(--c-green-dark);
  background: var(--c-green-light);
  border-radius: 999px;
  padding: 2px 9px;
  white-space: nowrap;
}

/* harvest list */
.wkf-bar {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 10px;
}
.wkf-bar-sp {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
  margin-left: auto;
}
.wkf-check {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tap);
  font-size: max(0.78125rem, var(--fs-min));
  color: var(--c-text-sec);
  margin-bottom: 10px;
}
.wkf-check input {
  width: 20px;
  height: 20px;
}
.wkf-bags {
  display: flex;
  flex-direction: column;
  max-height: 42dvh;
  overflow-y: auto;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
}
.wkf-bag {
  display: grid;
  /* minmax(0, 1fr), not 1fr: a grid track's default minimum is min-content, so
     a long bag id widens the track instead of ellipsing inside it, and the row
     pushes the modal sideways. */
  grid-template-columns: minmax(0, 1fr) auto 92px auto;
  gap: 9px;
  align-items: center;
  padding: 7px 11px;
  border-top: 1px solid var(--c-border-light);
}
.wkf-bag:first-child {
  border-top: 0;
}
.wkf-bag-id {
  font-family: ui-monospace, 'Cascadia Code', monospace;
  font-size: max(0.78125rem, var(--fs-min));
  font-weight: 650;
}
.wkf-prev {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
  white-space: nowrap;
}
.wkf-g {
  text-align: right;
  font-weight: 700;
}
.wkf-unit {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
}
.wkf-sum {
  display: flex;
  align-items: baseline;
  gap: 9px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 2px solid var(--c-border);
}
.wkf-sum b {
  font-size: 1.25rem;
  font-weight: 800;
}
.wkf-sum span {
  font-size: max(0.75rem, var(--fs-min));
  color: var(--c-text-muted);
}

/* move targets */
.wkf-zones {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 8px;
}
.wkf-zone {
  display: flex;
  align-items: center;
  gap: 9px;
  text-align: left;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  padding: 12px;
  cursor: pointer;
  font: inherit;
  color: var(--c-text);
  min-height: max(48px, var(--tap-min));
}
.wkf-zone.sel {
  border-color: var(--c-primary);
  background: var(--c-primary-light);
  box-shadow: 0 0 0 1px var(--c-primary);
}
.wkf-zone-dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  flex: none;
}
.wkf-zone-n {
  font-size: 0.8125rem;
  font-weight: 650;
}

/* receipt */
.wkf-receipt {
  display: flex;
  flex-direction: column;
  gap: 12px;
  border: 2px solid var(--c-primary);
  border-radius: var(--radius);
  padding: 16px 18px;
}
.wkf-r-head {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 1rem;
}
.wkf-r-tick {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--c-primary);
  color: #fff;
  display: grid;
  place-items: center;
  font-size: 0.8125rem;
  font-weight: 800;
  flex: none;
}
.wkf-r-lines {
  display: flex;
  flex-direction: column;
  gap: 6px;
  border-top: 1px solid var(--c-border);
  padding-top: 12px;
}
.wkf-r-lines div {
  display: flex;
  gap: 10px;
  font-size: 0.8125rem;
  align-items: baseline;
}
.wkf-r-k {
  color: var(--c-text-muted);
  min-width: 116px;
  flex: none;
}

/* The flow on a phone. It used to answer three different questions at three
   invented widths — 460 for the two-up field pairs, 520 for the launcher tiles,
   560 for the list rows — none of which is where anything actually stops
   fitting. One breakpoint, at the 768 the rest of the app already uses. */
@media (max-width: 768px) {
  .wkf-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .wkf-row-meta {
    grid-column: 1 / -1;
  }
  .wkf-two,
  .wkf-zones {
    grid-template-columns: 1fr;
  }
  /* Three columns and four children, placed rather than left to auto-flow: id,
     field and unit on one line, and what the bag already gave on the next.
     The 560px rule hid that line instead — which is the number a worker needs
     to decide whether this bag is worth weighing at all. */
  .wkf-bag {
    grid-template-columns: minmax(0, 1fr) 84px auto;
  }
  .wkf-bag-id {
    grid-area: 1 / 1 / 2 / 2;
  }
  .wkf-g {
    grid-area: 1 / 2 / 2 / 3;
  }
  .wkf-unit {
    grid-area: 1 / 3 / 2 / 4;
  }
  .wkf-prev {
    grid-area: 2 / 1 / 3 / 4;
  }
  /* Label above value instead of beside it — a 116px label column leaves 200px
     for a zone name on a 375px screen. */
  .wkf-r-lines div {
    flex-wrap: wrap;
  }
  .wkf-r-k {
    min-width: 100%;
  }
  /* §5.2: one primary action, full width, in the bottom third. The primary is
     the last child, so a column puts it closest to the thumb; a row put it in
     the far corner at whatever width its label happened to be. */
  .wkf-foot {
    flex-direction: column;
  }
  .wkf-foot .btn {
    width: 100%;
  }
}

/* Knopfreihe des Etiketten-Dialogs: die Hauptsache zuerst und breit, damit sie
   am Tablet mit dem Daumen zu treffen ist, ohne die Beschriftung umzubrechen. */
.nbp-btns {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 16px;
}
.nbp-btns > .btn {
  justify-content: center;
  padding: 12px 16px;
}
.nbp-sub {
  display: flex;
  gap: 8px;
}
.nbp-sub .btn {
  flex: 1;
  justify-content: center;
}

/* The bag chip in an expanded batch row is the click path into the bag sheet. */
.bag-open-chip:hover {
  border-color: var(--c-primary) !important;
  background: var(--c-primary-light) !important;
}

/* Substrat ansetzen und Charge anlegen stehen nebeneinander: der Ansatz wird
   gemischt und die Charge zieht sofort daraus, also gehören sie in einen Blick.
   Der Substrat-Block ist schmaler, weil er weniger zu fragen hat. Unter 1000 px
   fällt beides untereinander — im Labor wird am Handy gearbeitet. */
/* Der geführte Weg zum Anlegen einer Charge: dieselbe Hülle wie Ernte und Umzug,
   nur mit eigenen Bausteinen. Alle Bedienelemente sind fingerhoch, weil der
   Ablauf im Labor am Tablet läuft. */
.wkf-q {
  font-size: 1rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  margin-bottom: 2px;
}
.wkf-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 10px;
}
.wkf-field > label {
  font-size: max(0.71875rem, var(--fs-min));
  color: var(--c-text-sec);
  font-weight: 600;
  margin: 0;
}
/* Outranks the app-wide `input, select, textarea { min-height: 64px }` on
   specificity, so before the floor this rule made the flow's fields SMALLER
   than every other field in the app — 44px against 64px — on the one screen
   that is filled in wearing gloves. */
.wkf-field input,
.wkf-field select {
  min-height: max(44px, var(--tap-min));
}
.wkf-two {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
/* "Mischung" mit dem Rückweg zum Rezept daneben, im Substrat-Arbeitsgang. Die
   Beschriftung sieht aus wie die der anderen Felder; der Knopf erscheint erst,
   wenn eine Zahl vom Rezept abweicht. */
.wkm-blend-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
}
.wkm-blend-head label {
  font-size: max(0.71875rem, var(--fs-min));
  color: var(--c-text-sec);
  font-weight: 600;
  margin: 0;
}
.wkm-blend-head [hidden] {
  display: none;
}
/* Vier Anteile, zwei je Zeile: am Telefon passen nicht mehr nebeneinander, und
   ein auto-fit-Raster bricht dort drei plus eins um. */
.wkm-blend label {
  color: var(--c-text-sec);
}
.wkf-chipsrow {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.wkf-w {
  border: 1px solid var(--c-border);
  background: var(--c-surface);
  border-radius: 8px;
  padding: 0 12px;
  min-height: max(44px, var(--tap-min));
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 650;
  color: var(--c-text-sec);
  cursor: pointer;
}
.wkf-w.on {
  background: var(--c-primary);
  border-color: var(--c-primary);
  color: #fff;
}
.wkf-note {
  background: var(--c-primary-light);
  border: 1px solid var(--c-green-border);
  color: var(--c-green-dark);
  border-radius: 9px;
  padding: 10px 12px;
  font-size: max(0.78125rem, var(--fs-min));
  line-height: 1.6;
  margin-bottom: 10px;
}
.wkf-note.bad {
  background: var(--c-red-light);
  border-color: var(--c-red-border);
  color: var(--c-red-dark);
}
.wkf-note b {
  display: block;
}
.wkf-link {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-decoration: underline;
  font-weight: 700;
  cursor: pointer;
}
.wkf-sum-card {
  border: 1px solid var(--c-border);
  border-radius: 9px;
  padding: 12px 14px;
  margin-bottom: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 0.8125rem;
}
.wkf-sum-card > div {
  display: flex;
  gap: 8px;
  align-items: baseline;
  flex-wrap: wrap;
}
.wkf-hint {
  font-size: max(0.6875rem, var(--fs-min));
  color: var(--c-text-muted);
  margin-bottom: 10px;
}

/* Stamm und "Angesetzt am": derselbe Baustein im Schnelldialog und im
   Chargen-Assistenten. Auswahl oder Feld nimmt die Breite, der Knopf daneben
   nur seine eigene — am Telefon bleibt beides in einer Zeile, ohne dass der
   Knopf zum Balken wird. */
.stamm-row {
  display: flex;
  gap: 6px;
  align-items: stretch;
}
.stamm-row > input,
.stamm-row > select {
  flex: 1 1 auto;
  min-width: 0;
}
/* Was als Beimpfmaterial gescannt oder gewählt ist, je ein Kärtchen mit ✕.
   Leer nimmt die Reihe keinen Platz. */
.inoc-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 6px;
}
.inoc-chips:empty {
  display: none;
}
.stamm-row > .btn {
  flex: none;
}
/* Im Schnelldialog stehen die Felder mit 8px Abstand untereinander; im
   Assistenten sorgt .wkf-field selbst dafür. Leer — ohne Sorte — nimmt der Platz
   gar keinen Raum. */
#ms-quick-modal .stamm-wrap:not(:empty),
#ms-quick-modal .anlage-wrap:not(:empty) {
  margin-top: 8px;
}
#wkf-body .anlage-wrap:not(:empty) {
  margin-bottom: 10px;
}
.anlage-zu {
  display: flex;
  gap: 6px;
  align-items: baseline;
  flex-wrap: wrap;
}
.anlage-k {
  color: var(--c-text-muted);
}

/* Die Karte "Stämme" auf der Sortenseite: je Sorte ihre Stämme, und darunter,
   was an Chargen und Kulturen steht, ohne in der Liste zu sein. */
.stamm-hint {
  color: var(--c-text-muted);
  margin-bottom: 10px;
}
.stamm-add {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: stretch;
  margin-bottom: 12px;
}
.stamm-add > select {
  flex: 1 1 180px;
  min-width: 0;
}
.stamm-add > input {
  flex: 2 1 200px;
  min-width: 0;
}
.stamm-add > .btn {
  flex: none;
}
.stamm-gruppe {
  border-top: 1px solid var(--c-border);
  padding: 10px 0;
}
.stamm-sorte {
  font-weight: 600;
  margin-bottom: 4px;
}
.stamm-kz {
  font-family: monospace;
  color: var(--c-text-muted);
}
/* Name und Verwendung links übereinander, die Knöpfe rechts: so bleibt eine
   Zeile eine Zeile, auch wenn der Name lang ist — als Flex-Reihe brach am
   Telefon das Kreuz allein in die nächste. */
.stamm-zeile {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 8px;
  padding: 3px 0;
}
.stamm-name {
  min-width: 0;
  overflow-wrap: anywhere;
}
.stamm-use {
  display: block;
}
.stamm-use,
.stamm-fremd,
.stamm-leer {
  color: var(--c-text-muted);
}
.stamm-fremd {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  margin-top: 6px;
}

/* Kartenkopf mit Motiv: derselbe Strich wie auf den Arbeitsgang-Kacheln, damit
   Substrat ansetzen und Growbags Batch auf einen Blick auseinandergehen. Die
   Zeichnung sitzt hinter der Überschrift und läuft aus der Ecke heraus. */
.card-head {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: linear-gradient(120deg, var(--wk-cl) 0%, transparent 70%);
  padding: 10px 12px;
  margin: -4px -4px 10px;
}
.card-head-art {
  position: absolute;
  right: -6px;
  bottom: -14px;
  width: 96px;
  color: var(--wk-c);
  pointer-events: none;
}
.card-head-art svg {
  width: 100%;
  height: auto;
  display: block;
}
.card-head .sec {
  position: relative;
}

/* Die Fußknöpfe der Abläufe sind die Hauptaktion und werden im Labor mit dem
   Finger getroffen — 34 px reichten dafür nicht. 44 reichen für die Maus, für
   den Handschuh nicht; max() hebt sie am Gerät an und lässt sie am Schreibtisch
   stehen. */
.wkf-foot .btn {
  min-height: max(44px, var(--tap-min));
}

/* ── Vorhaben (Forschung · Produkte · Bau) ───────────────────
   Drei Unterlagen zum Lesen, nicht zum Ausfüllen: eine Recherche, ein paar
   Rezepte und ein Bauplan. Das ist die einzige Stelle dieser Anwendung, an der
   längerer Fließtext steht, und Fließtext braucht anderes als eine Tabelle —
   eine Zeilenhöhe, mit der man einen Absatz zu Ende liest, und eine Zeile, die
   nicht quer über einen 27-Zoll-Schirm läuft.

   Die Tabellen bringen ihr Telefonverhalten selbst mit: sie tragen .t-cards und
   werden unter 769px zu Karten wie die zwölf anderen. Deshalb steht hier keine
   einzige Tabellenregel für das Telefon. */
.pj-head {
  border-left: 3px solid var(--c-primary);
}
.pj-title {
  font-size: max(1.0625rem, var(--fs-min));
  font-weight: 700;
  line-height: 1.25;
  margin-bottom: 4px;
}
.pj-meta {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  margin-bottom: 10px;
}
.pj-lead {
  font-size: max(0.875rem, var(--fs-min));
  line-height: 1.6;
  margin: 0;
  max-width: 68ch;
}
/* Die Zwischenüberschrift eines Abschnitts. .sec ist die Überschrift der Karte
   und schon vergeben; diese hier steht darunter und ist deshalb leiser gesetzt,
   aber nicht kleiner — unter dem Mindestmaß fängt das Suchen an. */
.pj-h {
  font-size: max(0.8125rem, var(--fs-min));
  font-weight: 700;
  color: var(--c-text);
  margin: 18px 0 6px;
}
.pj-sec > .pj-h:first-of-type {
  margin-top: 0;
}
.pj-p {
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.65;
  color: var(--c-text);
  margin: 0 0 10px;
  max-width: 72ch;
}
.pj-ul {
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.6;
  color: var(--c-text);
  margin: 0 0 10px;
  padding-left: 20px;
  max-width: 72ch;
}
.pj-ul li {
  margin-bottom: 6px;
}
/* Der Einschub. Zwei Tonlagen: bernstein für „das ist der Haken an der Sache",
   rot für „das geht schief, wenn du es überliest". Mehr Abstufungen wären eine
   Farbenlehre, die niemand liest. */
.pj-note {
  background: var(--c-amber-light);
  border-left: 3px solid var(--c-amber);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 10px 12px;
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.55;
  color: var(--c-text);
  margin: 0 0 10px;
  max-width: 72ch;
}
.pj-note-warn {
  background: var(--c-red-light);
  border-left-color: var(--c-red);
}
.pj-t {
  margin-bottom: 12px;
}
/* Prosa in einer Zelle: oben ausgerichtet, sonst schwimmt eine einzeilige
   Spalte in der Mitte einer vierzeiligen. */
.pj-t td {
  vertical-align: top;
  line-height: 1.5;
  /* Die Artentabelle hat sechs Spalten. Ohne das schiebt ein langes Wort wie
     „Verbundwerkstoff" die Tabelle am schmalsten Schreibtisch über ihre Karte
     hinaus — anywhere zählt beim Spaltenrechnen mit, break-word nicht. */
  overflow-wrap: anywhere;
}
/* Rezeptkarten. auto-fit mit 232px heißt: drei nebeneinander am Schreibtisch,
   eine untereinander am Telefon, ohne eine einzige Abfrage nach der Fensterbreite. */
.pj-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(232px, 100%), 1fr));
  gap: 10px;
  margin-bottom: 12px;
}
.pj-rx {
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-bg);
  padding: 12px 14px;
}
.pj-rx-t {
  font-weight: 700;
  font-size: max(0.8125rem, var(--fs-min));
  margin-bottom: 2px;
}
.pj-rx-sub {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  margin-bottom: 8px;
}
.pj-ing {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: max(0.8125rem, var(--fs-min));
}
.pj-ing li {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 3px 0;
  border-bottom: 1px solid var(--c-border-light);
  line-height: 1.4;
}
.pj-ing li:last-child {
  border-bottom: 0;
}
.pj-v {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}
.pj-rx-foot {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--c-border);
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  line-height: 1.45;
}
/* Der Ablauf über der Zeit: links wann, rechts was. Unter 769px stapelt sich
   das, weil „Tag 12–13" und ein Satz nebeneinander auf einem Telefon zwei
   Spalten von je elf Zeichen wären. */
.pj-steps {
  margin-bottom: 12px;
  border-top: 1px solid var(--c-border-light);
}
.pj-step {
  display: grid;
  grid-template-columns: 104px 1fr;
  gap: 14px;
  padding: 9px 0;
  border-bottom: 1px solid var(--c-border-light);
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.55;
}
.pj-step-k {
  font-weight: 700;
  color: var(--c-primary);
}
/* Ein Schritt mit einem Knopf daneben. Der Knopf bleibt so breit wie sein Wort
   — eine Zeile, die zur Aufgabe werden kann, ist kein Formular-Absenden. */
.pj-tasks {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: max(0.8125rem, var(--fs-min));
}
.pj-tasks li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid var(--c-border-light);
  line-height: 1.5;
}
.pj-tasks li:last-child {
  border-bottom: 0;
}
.pj-tasks li > span {
  flex: 1 1 200px;
  min-width: 0;
}
.pj-add {
  flex: 0 0 auto;
}
/* Der Anfragetext, so wie er in die Mail gehört. pre-wrap, damit die Zeilen
   umbrechen statt die Seite breiter zu machen: ein <pre> mit einem deutschen
   Absatz darin ist sonst 900px breit und das Telefon 360. */
.pj-pre {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  background: var(--c-bg);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  padding: 12px;
  font-family: inherit;
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.55;
  color: var(--c-text);
  margin: 0 0 10px;
}
@media (max-width: 768px) {
  .pj-step {
    grid-template-columns: 1fr;
    gap: 2px;
  }
}

/* Der Tagesplan. Eine Zeile ist ein Handgriff, und abgehakt heisst weg: am
   Tisch zeigt die Liste, was noch zu tun ist. Durchgestrichene Zeilen kosten
   genau dort Platz, wo der nächste Schritt stehen müsste. */
.pj-chk {
  display: flex;
  gap: 11px;
  align-items: flex-start;
  padding: 10px 0;
  border-bottom: 1px solid var(--c-border-light);
}
.pj-chk input {
  margin-top: 3px;
  width: 18px;
  height: 18px;
  flex: none;
  accent-color: var(--c-primary);
  cursor: pointer;
}
.pj-chk label {
  cursor: pointer;
  flex: 1;
  min-width: 0;
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.5;
}
.pj-body {
  flex: 1;
  min-width: 0;
}
.pj-chk input:checked + .pj-body > label {
  opacity: 0.45;
  text-decoration: line-through;
}
/* Das Detail zur Zeile. Zu, bis jemand danach fragt — die Mengen stehen dort,
   die Begruendung auch, und beides braucht man einmal und dann nie wieder. */
.pj-more {
  border: 0;
  background: none;
  color: var(--c-text-muted);
  cursor: pointer;
  font: inherit;
  line-height: 1;
  padding: 0 6px;
  min-width: max(24px, var(--tap-min));
  min-height: max(24px, var(--tap-min));
}
.pj-more:hover {
  color: var(--c-text);
}
.pj-det {
  margin-top: 4px;
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--c-text-muted);
}
.pj-chk.is-done {
  display: none;
}
.pj-plan.pj-show-done .pj-chk.is-done {
  display: flex;
}
/* Wo der Handgriff passiert: Einkauf, Bank, Autoklav, Flow. Eine eigene Zeile
   über dem Text, weil sie am Telefon sonst die halbe Breite nimmt. */
.pj-tag {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--c-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 2px;
}
.pj-empty {
  margin: 0;
  padding: 14px 0 2px;
  font-size: max(0.8125rem, var(--fs-min));
  color: var(--c-text-muted);
}
.pj-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
.pj-count {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
/* Nachschlagen, nicht lesen. Die Recherche steht unter dem Tagesplan und ist
   zu: aufgeklappt ist sie ein Bericht, zugeklappt eine Zeile, und während der
   Autoklav läuft will niemand einen Bericht scrollen. */
.pj-fold > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  min-height: max(24px, var(--tap-min));
}
.pj-fold > summary::-webkit-details-marker {
  display: none;
}
.pj-fold > summary::before {
  content: '▸';
  flex: none;
  font-size: 0.75rem;
  color: var(--c-text-muted);
}
.pj-fold[open] > summary::before {
  content: '▾';
}
.pj-fold[open] > summary {
  margin-bottom: 10px;
}
.pj-fold > summary:focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ── Versuch und Ansätze ─────────────────────────────────────
   Eine Zeile je Ansatz: Code, was es ist, Ergebnis, Ausgang, Etikett. Flex mit
   Umbruch statt Rasterspalten — dieselbe Zeile liegt am Schreibtisch
   nebeneinander und am Telefon untereinander, ohne eine zweite Beschreibung
   desselben Layouts in einer Medienabfrage. */
.pj-v-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 10px;
}
.pj-v-code {
  font-weight: 700;
  font-size: max(0.875rem, var(--fs-min));
}
.pj-v-title {
  font-size: max(0.875rem, var(--fs-min));
}
.pj-v-meta {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  margin-top: 2px;
}
/* Drucken und Abschliessen stehen zusammen rechts im Kopf und brechen am
   Telefon gemeinsam unter den Titel. */
.pj-v-knoepfe {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* Der Zeitraum fuers Laborbuch: zwei Datumsfelder, die Beschriftung darueber,
   der Knopf auf Hoehe der Felder. */
.pj-lb-bar {
  align-items: flex-end;
}
.pj-lb-feld {
  flex: 1 1 130px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
.pj-lb-feld input,
.pj-lb-feld select {
  width: 100%;
}
/* Das Arbeitspaket des Versuchs, unter dem Kopf: Beschriftung und Auswahl in
   einer Zeile, am Telefon untereinander. */
.pj-ap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  margin-bottom: 6px;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
.pj-ap select {
  flex: 1 1 200px;
  min-width: 0;
}
/* Ein Paket je Zeile: der Code schmal, der Titel breit, der Knopf rechts.
   Die Zeile zum Anlegen steht genauso, deshalb gegen .pj-run-new input. */
.pj-sec .pj-ap-code {
  flex: 0 1 90px;
  min-width: 0;
}
.pj-sec .pj-ap-titel {
  flex: 1 1 180px;
  min-width: 0;
}
.pj-ap-weg input {
  color: var(--c-text-muted);
}
.pj-run {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid var(--c-border-light);
}
.pj-run-kopf {
  flex: 1 1 100%;
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
}
.pj-run-tools {
  flex: 1 1 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.pj-run-code {
  flex: 0 0 auto;
  font-weight: 700;
  font-size: max(0.8125rem, var(--fs-min));
  /* Ein langer Code bricht um, statt die Zeile ueber den Rand zu schieben: neben
     ihm stehen noch das Etikett und das Kreuz zum Entfernen. */
  max-width: 40%;
  overflow-wrap: anywhere;
}
.pj-run-what {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
/* Das Etikett steht oben neben dem Code und nicht unten neben der Auswahl:
   unten nahm es dem Ergebnisfeld die Breite, und getippt wird ins Feld. */
.pj-run-etikett {
  flex: 0 0 auto;
  margin-left: auto;
}
.pj-run-res {
  /* Klein starten, dann waechst es in den Rest. 150 px Basis hiessen am Telefon:
     Feld, Auswahl und Etikett brauchen zusammen 346 von 330 verfuegbaren, und
     der Knopf brach in eine dritte Zeile um. */
  flex: 1 1 90px;
  min-width: 0;
}
.pj-run-new {
  border-bottom: 0;
  padding-top: 12px;
}
.pj-run-new input {
  flex: 1 1 130px;
  min-width: 0;
}
.pj-out {
  flex: 0 0 auto;
  width: auto;
}
.pj-run-ro .pj-run-res {
  flex: 2 1 150px;
  font-size: max(0.8125rem, var(--fs-min));
}
.pj-out-good {
  color: var(--c-green-dark);
}
.pj-out-contam {
  color: var(--c-red-dark);
}
.pj-out-discarded,
.pj-out-open {
  color: var(--c-text-muted);
}

/* ── Fotos am Versuch ────────────────────────────────────────
   Quadratische Kacheln in einem Raster, das sich selbst fuellt: am Telefon
   drei nebeneinander, am Schreibtisch acht, ohne eine zweite Beschreibung
   desselben Rasters. Darunter steht, wann das Bild entstanden ist — bei einem
   Lauf ueber vierzehn Tage ist das die halbe Information. */
.pj-fotos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(96px, 100%), 1fr));
  gap: 8px;
  margin-bottom: 12px;
}
.pj-foto {
  position: relative;
  margin: 0;
}
.pj-foto img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--c-border);
  background: var(--c-bg);
}
.pj-foto figcaption {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  margin-top: 3px;
  line-height: 1.3;
}
/* The full-resolution original, as a link in the caption. The padding makes
   it something a thumb can hit; the negative margin keeps the line where it
   was. Where there is none yet, the same place offers to add it: a button,
   since it opens a file picker and not a page, dressed like the link. */
.pj-foto-original,
.pj-foto-original-neu {
  display: inline-block;
  padding: 6px 4px;
  margin: -6px 0;
  color: var(--c-green-dark);
  font-weight: 600;
}
.pj-foto-original-neu {
  border: 0;
  background: none;
  font-family: inherit;
  font-size: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.pj-foto-weg {
  position: absolute;
  top: 3px;
  right: 3px;
  min-width: max(24px, var(--tap-min));
  min-height: max(24px, var(--tap-min));
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

/* ── Die Liste der Experimente ───────────────────────────────
   Eine Zeile je Ansatz, und die ganze Zeile ist der Knopf: am Telefon trifft
   man einen Bereich ueber die volle Breite, nicht ein Pfeilchen am Rand. */
.pj-liste-zeile {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  border: 0;
  border-bottom: 1px solid var(--c-border-light);
  background: none;
  font: inherit;
  color: inherit;
  padding: 11px 0;
  cursor: pointer;
  min-height: max(44px, var(--tap-min));
}
.pj-liste-zeile:last-child {
  border-bottom: 0;
}
.pj-liste-zeile:hover {
  background: var(--c-bg);
}
.pj-liste-code {
  flex: 0 0 auto;
  font-weight: 700;
  font-size: max(0.8125rem, var(--fs-min));
  min-width: 34px;
}
.pj-liste-mitte {
  flex: 1 1 auto;
  min-width: 0;
}
.pj-liste-titel {
  display: block;
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.35;
}
.pj-liste-meta {
  display: block;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  line-height: 1.35;
}
.pj-liste-rechts {
  flex: 0 0 auto;
  text-align: right;
}
.pj-liste-pfeil {
  flex: 0 0 auto;
  color: var(--c-text-muted);
}
/* Der Weg zurueck. Steht ueber dem Titel, weil dort die Hand ohnehin ist,
   wenn sie gerade hineingetippt hat. */
.pj-zurueck {
  border: 0;
  background: none;
  color: var(--c-text-muted);
  font: inherit;
  font-size: var(--fs-xs);
  cursor: pointer;
  padding: 0 8px 4px 0;
  min-height: max(24px, var(--tap-min));
}
.pj-zurueck:hover {
  color: var(--c-text);
}

/* ── Notizen ─────────────────────────────────────────────────
   Ein Zettel neben dem Laborbuch. Ein Ansatz traegt sein Ergebnis in einer
   Zeile, damit die Tabelle zu ueberfliegen bleibt; alles, was laenger ist,
   steht hier. Hinter derselben Tuer liegt die Recherche, weil beide dasselbe
   sind: nachschlagen, nicht lesen. */
.pj-notebox textarea {
  width: 100%;
  margin-top: 8px;
  font: inherit;
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.5;
  padding: 10px 12px;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-bg);
  color: var(--c-text);
  resize: vertical;
}
/* Der Knopf bleibt so breit wie sein Wort. */
.pj-note-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}
.pj-notes {
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
}
.pj-note {
  padding: 12px 0;
  border-top: 1px solid var(--c-border-light);
}
.pj-note-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 6px;
}
.pj-note-date {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
.pj-note-btns {
  display: flex;
  gap: 8px;
}
/* Zeilenumbrueche bleiben Zeilenumbrueche: eine Notiz wird getippt, nicht
   gesetzt. Der Text geht durch pjText, Markup entsteht daraus keins. */
.pj-note-body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.55;
}
.pj-note .pj-note-edit,
.pj-note .pj-note-bar {
  display: none;
}
.pj-note.is-edit .pj-note-body {
  display: none;
}
.pj-note.is-edit .pj-note-edit {
  display: block;
}
.pj-note.is-edit .pj-note-bar {
  display: flex;
}
/* Die Recherche sitzt jetzt hinter den Notizen statt als dreizehn eigene
   Karten darunter: eine Tuer, dahinter die Schubladen. */
.pj-refs {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--c-border);
}
.pj-ref {
  margin-top: 10px;
  padding-left: 14px;
  border-left: 2px solid var(--c-border-light);
}

/* ── Verlauf ─────────────────────────────────────────────────
   Wer wann was an einem Experiment geaendert hat, das Neueste oben. Der Kopf
   klein und grau wie das Datum einer Notiz. Der alte Wert durchgestrichen, der
   neue schlicht, beide mit ihren Zeilenumbruechen: ein Fazit hat Absaetze, und
   ein Verlauf, der sie zusammenzieht, zeigt einen Text, den es nie gab. */
.pj-hist-liste {
  list-style: none;
  margin: 0;
  padding: 0;
}
.pj-hist {
  padding: 10px 0;
  border-top: 1px solid var(--c-border-light);
}
.pj-hist:first-child {
  border-top: 0;
  padding-top: 0;
}
.pj-hist-kopf {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
}
.pj-hist-was {
  font-size: max(0.8125rem, var(--fs-min));
  font-weight: 600;
  margin-top: 2px;
}
.pj-hist-wert {
  font-size: max(0.8125rem, var(--fs-min));
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin-top: 2px;
}
.pj-hist-wert del {
  color: var(--c-text-muted);
}
.pj-hist-wert ins {
  text-decoration: none;
}

/* ── Entfernen und Zurueckholen ──────────────────────────────
   Das Kreuz am Gefaess steht ganz rechts neben dem Etikett, so leise wie das
   am Foto: entfernt wird selten, und eine Rueckfrage kommt ohnehin. Was
   entfernt ist, liegt zugeklappt unter der Liste. Ein entferntes Gefaess ist
   nur noch zum Lesen da (.pj-run-ro), ein entferntes Foto blass, und beide
   tragen statt des Kreuzes einen Knopf zum Zurueckholen. */
.pj-run-weg {
  flex: 0 0 auto;
  min-width: max(24px, var(--tap-min));
  min-height: max(24px, var(--tap-min));
  border: 0;
  background: none;
  color: var(--c-text-muted);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
}
.pj-run-weg:hover {
  color: var(--c-red-dark);
}
.pj-run-zurueck {
  flex: 0 0 auto;
  margin-left: auto;
}
.pj-weg {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--c-border-light);
}
.pj-foto-entfernt img {
  opacity: 0.55;
}
/* Das Foto, von dem eine Zeile im Verlauf spricht, klein davor. */
.pj-hist-foto {
  width: 28px;
  height: 28px;
  object-fit: cover;
  border-radius: 4px;
  vertical-align: middle;
  margin-right: 6px;
}
/* Ein endgueltig geloeschtes Foto: der Platz, an dem es war. */
.pj-foto-leer {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  padding: 6px;
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  border: 1px dashed var(--c-border);
  border-radius: var(--radius-sm);
}
.pj-foto-zurueck {
  display: block;
  width: 100%;
  margin-top: 4px;
}

/* Settings → Backup → USB stick. The key box is the one thing on the page that
   must not be overlooked, so it is the only one with a coloured frame. */
.usb-intro {
  font-size: 0.8125rem;
  color: var(--c-text-sec);
  margin-bottom: 12px;
  line-height: 1.6;
}
.usb-status {
  font-weight: 600;
  margin-bottom: 6px;
}
.usb-msg {
  font-size: 0.8125rem;
  line-height: 1.5;
  margin: 6px 0;
}
.usb-msg-err {
  color: var(--c-red-dark);
}
.usb-msg-ok {
  color: var(--c-green-dark);
}
.usb-progress {
  margin: 8px 0;
}
.usb-bar {
  display: block;
  width: 100%;
  height: 10px;
  margin-bottom: 4px;
}
.usb-sticks {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
}
.usb-stick {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 0;
  border-top: 1px solid var(--c-border);
}
.usb-stick-main {
  min-width: 0;
}
.usb-stick-name {
  font-weight: 600;
}
.usb-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--c-green-light);
  color: var(--c-green-dark);
  font-size: 0.75rem;
  font-weight: 600;
}
.usb-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin-top: 10px;
}
.usb-row input[type='text'] {
  flex: 1;
  min-width: 160px;
}
.usb-sub {
  font-weight: 700;
  margin-bottom: 4px;
}
#usb-setup {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--c-border);
}
#usb-setup[hidden],
#usb-key[hidden] {
  display: none;
}
.usb-drives {
  display: grid;
  gap: 6px;
  margin: 8px 0;
}
.usb-drive {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  min-height: 44px;
  padding: 6px 10px;
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  cursor: pointer;
}
.usb-drive.is-off {
  opacity: 0.6;
  cursor: default;
}
.usb-drive-root {
  font-weight: 600;
}
.usb-key {
  margin: 0 0 14px;
  padding: 12px;
  border: 2px solid var(--c-amber-border);
  border-radius: var(--radius);
  background: var(--c-amber-light);
}
.usb-key-code {
  display: block;
  margin: 8px 0;
  padding: 10px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.125rem;
  letter-spacing: 0.04em;
  word-break: break-all;
  background: var(--c-surface);
  color: var(--c-text);
  border-radius: 6px;
  user-select: all;
}
.usb-key-saved {
  display: flex;
  gap: 9px;
  align-items: center;
  margin-top: 10px;
  font-size: 0.8125rem;
  cursor: pointer;
}
/* The general input rule (full width, 64px tall, padded) is for text fields;
   a radio or a checkbox under it is a 64px column with a small circle in it. */
.usb-drive input,
.usb-key-saved input {
  width: 22px;
  height: 22px;
  min-height: 0;
  margin: 0;
  padding: 0;
  flex: none;
}