:root { --bg: #f2f7f4; --surface: #ffffff; --surface-2: #eaf2ec; --border: #dde8e1; --border-strong: #c9d6cd; --text: #17211b; --muted: #6a7c71; --accent: #3f9c72; --accent-hover: #34825f; --accent-soft: #e6f3ec; --danger: #c0392b; --danger-soft: #fbeceb; --warn: #b06f14; --warn-soft: #fbf3e6; --ok: #2e7d55; --radius: 7px; --radius-sm: 5px; --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px; --sp-6: 32px; --sidebar-w: 236px; --shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06); --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } * { box-sizing: border-box; } html, body { height: 100%; } body { margin: 0; font-family: var(--font); background: var(--bg); color: var(--text); font-size: 14.5px; line-height: 1.5; -webkit-font-smoothing: antialiased; /* Pfeil als Standard – sonst zeigt der Browser über jedem Text den Text-Cursor (I-Beam), und alles sieht aus wie ein Eingabefeld. cursor wird vererbt. */ cursor: default; } /* Text-Cursor nur dort, wo man wirklich tippt. */ textarea, [contenteditable="true"], input:where([type="text"], [type="number"], [type="search"], [type="email"], [type="password"], [type="url"], [type="tel"]), input:not([type]) { cursor: text; } /* Hand für klar Klickbares, das noch keine eigene Regel hat (Buttons, Sortier- Header, Kacheln, Menüoptionen, klickbare Zeilen bringen sie schon selbst mit). */ a[href], select, summary, input[type="checkbox"], input[type="radio"], input[type="file"], .checklist label, .check-inline, .enforce-toggle { cursor: pointer; } /* Reiner UI-Text (Titel, Labels, Bedienelemente) lässt sich nicht markieren – beim Klicken poppt dort sonst ein blinkendes Text-Caret auf, das wie ein Eingabefeld wirkt. Inhalte, die man wirklich kopieren will, bleiben markierbar: Tabellen- zellen, Eingabefelder und Code/Token (sowie alles mit .selectable). */ body { -webkit-user-select: none; user-select: none; } .table td, input, textarea, [contenteditable="true"], code, pre, .selectable, .named-text { -webkit-user-select: text; user-select: text; } a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } /* ---------- Scrollleisten ans Theme anpassen ---------- Sonst blitzt in scrollbaren Karten/Tabellen die helle System-Scrollleiste durch. Nutzt die Theme-Variablen, passt sich also hell/dunkel an. */ * { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; } ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 8px; border: 2px solid transparent; /* schlankerer Daumen mit runden Enden */ background-clip: padding-box; } ::-webkit-scrollbar-thumb:hover { background: var(--muted); } ::-webkit-scrollbar-corner { background: transparent; } h1 { font-size: 1.35rem; font-weight: 650; letter-spacing: -0.01em; margin: 0; } h2 { font-size: 0.95rem; font-weight: 650; margin: 0 0 var(--sp-3); letter-spacing: -0.005em; } .icon { flex: 0 0 auto; vertical-align: middle; } /* ---------- App shell ---------- */ .app-shell { display: flex; min-height: 100vh; } .sidebar { width: var(--sidebar-w); flex: 0 0 var(--sidebar-w); background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh; } .sidebar-brand { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-5) var(--sp-4) var(--sp-4); font-weight: 680; font-size: 1.05rem; letter-spacing: -0.01em; } /* Eigenes Logo. Die Deckelung ist Absicht: Ein sehr grosses oder sehr breites Bild darf die Seitenleiste nicht auseinanderziehen. Das Seitenverhaeltnis bleibt durch object-fit erhalten, ueberstehendes wird nicht abgeschnitten. */ .brand-logo { display: block; max-width: 100%; max-height: 36px; width: auto; object-fit: contain; } /* Vorschau in den Einstellungen: Karomuster zeigt Transparenz. */ .brand-preview { display: grid; place-items: center; min-height: 64px; padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm); background-color: var(--bg); background-image: linear-gradient(45deg, var(--border) 25%, transparent 25%, transparent 75%, var(--border) 75%), linear-gradient(45deg, var(--border) 25%, transparent 25%, transparent 75%, var(--border) 75%); background-size: 14px 14px; background-position: 0 0, 7px 7px; } .brand-preview img { max-width: 100%; max-height: 56px; width: auto; object-fit: contain; } .sidebar-brand .mark { display: grid; place-items: center; width: 28px; height: 28px; border-radius: var(--radius-sm); background: var(--accent); color: #fff; } .sidebar-nav { display: flex; flex-direction: column; gap: 2px; padding: var(--sp-2); flex: 1; overflow-y: auto; } .nav-link { display: flex; align-items: center; gap: var(--sp-3); padding: 9px var(--sp-3); border-radius: var(--radius-sm); color: var(--muted); font-weight: 520; font-size: 0.9rem; } .nav-link:hover { background: var(--surface-2); color: var(--text); text-decoration: none; } .nav-link.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; } .nav-section { padding: var(--sp-4) var(--sp-3) var(--sp-1); font-size: 0.68rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); opacity: 0.7; } .sidebar-foot { border-top: 1px solid var(--border); padding: var(--sp-3); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); } .sidebar-user { display: flex; flex-direction: column; min-width: 0; } .sidebar-user .name { font-weight: 600; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .sidebar-user .role { font-size: 0.72rem; color: var(--muted); } .main { flex: 1; min-width: 0; display: flex; flex-direction: column; } .content { width: 100%; max-width: 1400px; margin: 0 auto; padding: var(--sp-6) var(--sp-5) 64px; } /* Das Dashboard-Raster soll die volle Breite nutzen, nicht auf Lesebreite kappen. */ .content--wide { max-width: none; } .page-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-5); flex-wrap: wrap; } .page-head .sub { color: var(--muted); font-size: 0.9rem; margin-top: 2px; } .actions { display: flex; gap: var(--sp-2); } /* ---------- Cards ---------- */ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--sp-5); box-shadow: var(--shadow); margin-bottom: var(--sp-4); } .card-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-4); } .card-head h2 { margin: 0; } .card-head .icon { color: var(--muted); } /* Zwei Spalten, solange beide genug Platz haben - sonst automatisch untereinander. auto-fit statt fester Umbruchbreite, damit es auch passt, wenn das Fenster nur die halbe Bildschirmbreite einnimmt. min(100%, …) verhindert, dass die Spalte auf schmalen Geraeten breiter wird als der Bildschirm. */ .grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: var(--sp-4); } /* Ohne min-width:0 schrumpfen Grid-Kinder nie unter ihre Inhaltsbreite - dadurch lief die Tabelle aus ihrer Karte heraus und wurde abgeschnitten. */ .grid-2 > * { min-width: 0; } .form-narrow { max-width: 600px; } /* Tabelle plus Block daneben: Die Tabelle hat mehr Spalten und bekommt deshalb den groesseren Anteil - sonst rutscht sie unnoetig frueh in die Kartenansicht, obwohl daneben Platz frei waere. */ /* Tabelle bekommt den ganzen restlichen Platz, der Block daneben bleibt schmal und gedeckelt - so scrollt die (breite) Tabelle nicht seitwaerts. */ .grid-2.wide-aside { grid-template-columns: minmax(0, 1fr) clamp(320px, 24%, 420px); align-items: start; } @media (max-width: 1000px) { .grid-2.wide-aside { grid-template-columns: minmax(0, 1fr); } } /* Artikelseite (Bearbeiten): links das Formular - dessen Felder brauchen keine halbe Seite -, rechts die Chargen-/Bestandstabelle, die den restlichen Platz bekommt, damit Menge/MHD/Lagerort ohne Seitwaerts-Scrollen nebeneinander passen. Spiegelbild von .wide-aside (dort steht die Tabelle links). */ .grid-2.form-plus-table { grid-template-columns: clamp(480px, 38%, 720px) minmax(0, 1fr); align-items: start; } @media (max-width: 1000px) { .grid-2.form-plus-table { grid-template-columns: minmax(0, 1fr); } } /* ---------- Forms ---------- */ label { display: block; margin-bottom: var(--sp-4); font-size: 0.8rem; color: var(--muted); font-weight: 580; } input, select { width: 100%; margin-top: 5px; padding: 8px 10px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); font-size: 0.9rem; font-family: inherit; background: var(--surface); color: var(--text); transition: border-color .12s, box-shadow .12s; } input:focus, select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } input::placeholder { color: var(--muted); opacity: 0.7; } /* Ankreuzfelder sind keine Textfelder: die Regel oben (width:100%, margin-top) zoege sie sonst ueber die volle Breite, das Haekchen landete in der Mitte und der Text daneben ganz rechts. Hier zentral ausgenommen, damit das nicht an jeder Einsatzstelle einzeln nachgeholt werden muss. */ input[type="checkbox"], input[type="radio"] { width: auto; margin: 0; flex: 0 0 auto; accent-color: var(--accent); } .row { display: flex; gap: var(--sp-3); align-items: flex-start; } .row .grow { flex: 1; } /* Artikelbild neben den Kennfeldern. min-width:0 an der Feldspalte, sonst drueckt ein langer Produktname die Eingaben aus der Karte heraus. */ .produkt-kopf { display: flex; gap: var(--sp-4); align-items: flex-start; } .produkt-kopf-felder { flex: 1 1 auto; min-width: 0; } /* Hochkant: Lebensmittelverpackungen sind fast immer hoeher als breit - in einem quadratischen Rahmen blieben links und rechts leere Streifen. */ .produkt-bild { flex: 0 0 auto; width: 132px; aspect-ratio: 3 / 4; object-fit: contain; /* Verpackungen nie beschneiden */ background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--sp-2); } .produkt-bild.clickable { cursor: zoom-in; } /* Auf schmalen Fenstern haben Bild und Felder nebeneinander keinen Platz mehr. */ @media (max-width: 560px) { .produkt-kopf { flex-direction: column-reverse; align-items: center; } } /* Bild-Lightbox: Klick aufs Artikelbild zeigt es gross (Schliessen per Knopf, Klick auf den Hintergrund oder Escape). */ .lightbox { position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center; padding: var(--sp-5); background: rgba(0, 0, 0, 0.82); } .lightbox-img { max-width: 92vw; max-height: 92vh; object-fit: contain; border-radius: var(--radius); box-shadow: 0 16px 56px rgba(0, 0, 0, 0.5); cursor: default; } .lightbox-close { position: fixed; top: 16px; right: 16px; display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: var(--surface); color: var(--text); border: 1px solid var(--border); cursor: pointer; } .lightbox-close:hover { background: var(--surface-2); } /* Knoepfe neben dem Barcode-Feld: nebeneinander, mit Umbruch auf schmalen Fenstern. Das Eingabefeld daneben fuellt die restliche Breite. */ .knopf-spalte { display: flex; flex-flow: row wrap; gap: var(--sp-2); flex: 0 1 auto; min-width: 0; } /* Kein nowrap: in einer schmalen Formularspalte darf ein langer Button-Text ("Mit Open Food Facts vergleichen") umbrechen statt abgeschnitten zu werden. */ .knopf-spalte .btn { justify-content: center; white-space: normal; min-width: 0; } /* Gegenueberstellung mit Open Food Facts */ .off-vergleich { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-2); padding: var(--sp-3); margin-bottom: var(--sp-4); } .off-kopf { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-3); } .off-kopf h3 { font-size: 0.95rem; margin: 0; flex: 1; } .off-tabelle { background: var(--surface); } /* Klein genug, dass die Zeile nicht auseinandergeht, gross genug zum Erkennen. */ .off-bild { width: 56px; aspect-ratio: 3 / 4; object-fit: contain; background: var(--surface-2); border-radius: 4px; display: block; } .off-neu { display: flex; align-items: center; gap: var(--sp-2); } .field-inline { display: flex; gap: var(--sp-2); align-items: flex-end; margin-bottom: var(--sp-4); } /* Labels in einer Inline-Reihe duerfen keinen eigenen Abstand haben, sonst rutschen daneben stehende Buttons optisch nach unten. */ .field-inline > label { margin-bottom: 0; } /* Verschachtelt in einem Label (z.B. Menge + Einheit) traegt das Label den Abstand. */ label .field-inline { margin-bottom: 0; } /* In Tabellenzellen (z.B. Chargen-Aktionen) keinen zusaetzlichen Abstand; bei wenig Platz lieber umbrechen als die Felder zusammenquetschen. */ td .field-inline { margin-bottom: 0; flex-wrap: wrap; } /* Eingaben haben global width:100% und wuerden im Flex-Layout zusammenschrumpfen. Zahlenfelder duerfen wachsen, Einheiten-Dropdowns behalten ihre Textbreite. */ .field-inline > input { flex: 1 1 auto; min-width: 78px; } .field-inline > select { flex: 0 0 auto; width: auto; min-width: 132px; } /* Auswahlfelder in Tabellen richten sich nach ihrem Text statt nach der Zelle. */ /* Auswahlfelder richten sich nach ihrem Text. Die fruehere Mindestbreite von 132px machte schmale Spalten (z.B. "Einheit") unnoetig breit. */ td select { width: auto; min-width: 0; max-width: 100%; } /* Button-Paare (z.B. Speichern/Abbrechen) gleich breit halten. */ /* Mindestbestände: eine Zeile je Eintrag in FESTEN Spalten. Vorher lag hier ein .field-inline – dort wachsen Name und Eingabefeld beide (.field-inline > input { flex: 1 1 auto }) und teilen sich den Restplatz. Die Felder standen dadurch je nach Namenslänge woanders. */ .min-zeile { display: grid; grid-template-columns: minmax(0, 1fr) 110px 150px 150px 160px 36px; gap: var(--sp-2); align-items: center; } .min-zeile > * { min-width: 0; } .min-zeile input, .min-zeile select { margin-top: 0; width: 100%; min-width: 0; } .min-zeile .bestand { text-align: right; } /* Schmal: Name über die volle Breite, darunter die Felder nebeneinander. */ @media (max-width: 900px) { .min-zeile { grid-template-columns: 1fr 1fr 1fr 36px; } .min-zeile > .name { grid-column: 1 / -1; } .min-zeile .bestand { text-align: left; } } .btn-pair { display: flex; gap: var(--sp-2); flex-wrap: wrap; justify-content: flex-end; } .btn-pair .btn { flex: 1 1 auto; min-width: 104px; justify-content: center; } /* ---------- Buttons ---------- */ .btn { display: inline-flex; align-items: center; gap: var(--sp-2); padding: 8px 14px; border: 1px solid var(--border-strong); background: var(--surface); border-radius: var(--radius-sm); font-size: 0.875rem; font-weight: 560; font-family: inherit; cursor: pointer; color: var(--text); transition: background .12s, border-color .12s, opacity .12s; white-space: nowrap; } .btn:hover { background: var(--surface-2); text-decoration: none; } .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; } .btn.primary:hover { background: var(--accent-hover); } .btn.danger { color: var(--danger); border-color: transparent; background: transparent; } .btn.danger:hover { background: var(--danger-soft); } .btn.ghost { border-color: transparent; background: transparent; color: var(--muted); } .btn.ghost:hover { background: var(--surface-2); color: var(--text); } .btn.sm { padding: 5px 9px; font-size: 0.8rem; } .btn:disabled { opacity: 0.55; cursor: default; } .btn-icon { padding: 6px; border-color: transparent; background: transparent; color: var(--muted); } .btn-icon:hover { background: var(--surface-2); color: var(--text); } .btn-icon.danger:hover { background: var(--danger-soft); color: var(--danger); } .link-btn { background: none; border: none; color: var(--accent); cursor: pointer; font: inherit; padding: 0; text-align: left; } .link-btn:hover { text-decoration: underline; } /* ---------- Tables ---------- */ /* container-type: Die Tabelle richtet sich nach dem Platz, den sie wirklich hat - nicht nach der Bildschirmbreite. So stellt sie sich auch um, wenn das Fenster nur die halbe Bildschirmbreite einnimmt oder ein Block danebensteht. */ .table-wrap { max-width: 100%; container-type: inline-size; overflow-x: auto; } .table { width: 100%; border-collapse: collapse; font-size: 0.875rem; } .table th, .table td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--border); } /* Frueher stand hier "overflow-wrap: anywhere" fuer ALLE Zellen. Das zerriss Woerter mitten drin ("PROD UKTE", "555 Gram m"), sobald eine Spalte knapp wurde. Umbrechen nur noch dort, wo wirklich lange Zeichenketten stehen. */ .break-anywhere { overflow-wrap: anywhere; } .nowrap { white-space: nowrap; } .table thead th { color: var(--muted); font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid var(--border-strong); white-space: nowrap; } /* Werte aus Zahl + Einheit gehoeren zusammen und duerfen nicht umbrechen. */ .table td.num { white-space: nowrap; } /* Eingaben in Tabellen kompakt halten - mit voller Feldhoehe blaeht sich jede Zeile auf und die Spalten werden unnoetig breit. */ .table td input, .table td select { padding: 5px 8px; font-size: 0.85rem; } .table td input[type="number"] { width: 8ch; min-width: 0; } /* Zelle mit Eingabe plus Kennzeichen (z.B. "niedrig"): eine Zeile, damit das Kennzeichen immer an derselben Stelle sitzt statt mal darunter zu rutschen. */ .cell-row { display: flex; align-items: center; gap: 6px; } /* Zwei Zeilen in einer Zelle: Steuerelement oben, Erläuterung darunter. */ .cell-col { display: flex; flex-direction: column; gap: 2px; align-items: stretch; } /* Beschriftung mit Erklaerung im Tooltip: die gepunktete Linie zeigt an, dass dort etwas steht - so bleibt das Formular kurz, ohne Wissen zu verstecken. */ .tip { border-bottom: 1px dotted var(--border-strong); } /* Auf-/Zuklappen von Unterkategorien */ .tree-toggle { padding: 2px; flex: 0 0 auto; } .tree-toggle .icon { transition: transform 0.12s ease; } .tree-toggle .icon.rotated { transform: rotate(90deg); } /* Platzhalter, damit Zeilen ohne Unterkategorien buendig zu denen mit stehen. */ .tree-spacer { display: inline-block; flex: 0 0 auto; width: 24px; } /* Aufklappbarer Kategorie-Baum als Auswahlfeld (CategorySelect). Der Knopf sieht aus wie ein