Finanzmanager/public/static/app.css
fds2610-bot 851e8204e9 Phase 3: Budgetplaner und Monatsabschluss
Budgetplaner mit Monatswerten je Kategorie, Monatsabschluss mit Prüflauf,
Snapshot und Buchungssperre. Dazu drei Vorarbeiten in der Oberfläche.

Budgetplaner
- Migration 004: budgets, budget_positionen, monatsabschluesse
- Zeilen sind der Kategoriebaum, Spalten die Monate; die ganze Tabelle wird in
  einem Formular gespeichert, damit mehrere Änderungen zusammen ankommen
- Zwei Ansichten: Verlauf (Jahressumme, abgelaufene Monate zusammengefasst,
  laufender Monat mit Budget/Ist/Saldo, danach Folgemonate) und alle Monate
- Ausgaben ohne Vorzeichen eingeben, es kommt aus der Kategorieart. Saldo ist
  Ist minus Budget: positiv heißt besser als geplant, negativ ist die
  Überschreitung und wird rot ausgewiesen
- Übergeordnete Kategorien summieren ihre Nachfahren und sind nicht beplanbar
- Ein Monat ohne Position ist "nicht geplant" und damit etwas anderes als
  "mit 0,00 geplant"
- Wert auf Folgemonate übertragen, Budget als Kopie ins neue Jahr

Monatsabschluss
- Prüflauf über nicht kategorisierte Buchungen, nicht abgeglichene Umsätze und
  Splitsummen-Differenzen; er hält niemanden auf, sondern zeigt den Stand
- Snapshot der Kontosalden und Kategoriesummen als JSON, bleibt auch nach einer
  Wiedereröffnung stehen
- Abgeschlossene Monate sperren Buchungen in beide Richtungen: weder hinein noch
  heraus verschieben. Abschaltbar über monatsabschluss_sperrt_buchungen

Vorarbeiten
- .knopf.primaer trug einen eigenen margin-top und stand dadurch in jeder
  Knopfleiste tiefer als seine Nachbarn - sichtbar an "Abbrechen" neben
  "Konto anlegen". Der Abstand liegt jetzt allein bei der Knopfleiste
- Kontengruppen werden wie Konten und Kategorien auf einer eigenen
  Formularseite gepflegt statt inline; dort hängt auch die Zuordnung der
  Konten (ankreuzen statt jedes Konto einzeln zu öffnen)
- Der QIF-Export enthält keine Kontengruppen: kein Feld im !Account-Eintrag,
  nichts in der QSD-Datei. Import-Oberfläche und Doku sagen das jetzt
- Die Baum-Einrückung lief über ein style-Attribut und wurde von der
  Content-Security-Policy verworfen; sie steht jetzt als data-ebene im Markup.
  Aus demselben Grund blendete app.js Erfolgsmeldungen nie aus

Tests: 11_BudgetTest, 12_MonatsabschlussTest (76 Prüfungen), E2E planung.spec.js.
Einzelheiten in docs/planung.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 07:31:25 +02:00

797 lines
20 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Finanzmanager – Grundgestaltung
*
* Layout wie im Lexware FinanzManager: feste Navigation links, Werkzeugleiste
* oben, darunter Tab-Leiste und dichte Datentabellen. Optik moderner: ruhigere
* Farben, mehr Weißraum, klare Typografie.
*
* Farben ausschließlich über Custom Properties. Die helle Palette steht auf
* blankem :root, die dunkle wird an zwei Stellen nachgezogen – einmal für die
* Systemvorgabe, einmal für die ausdrückliche Wahl im Einstellungsdialog.
*/
:root {
--grund: #f4f6f8;
--flaeche: #ffffff;
--flaeche-gedaempft: #eef1f5;
--rand: #d6dce4;
--rand-kraeftig: #b8c2cf;
--text: #1c2530;
--text-gedaempft: #5b6875;
--akzent: #1f6feb;
--akzent-text: #ffffff;
--akzent-schwach: #e5efff;
--navi-grund: #24313f;
--navi-text: #cdd7e2;
--navi-text-aktiv: #ffffff;
--navi-aktiv: #1f6feb;
--positiv: #1a7f4b;
--negativ: #c0392b;
--warnung: #9a6700;
--schatten: 0 1px 2px rgba(16, 24, 40, .06), 0 1px 3px rgba(16, 24, 40, .1);
--radius: 6px;
--schrift: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--schrift-zahl: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-thema="hell"]) {
--grund: #161b22;
--flaeche: #1c2330;
--flaeche-gedaempft: #232b39;
--rand: #303a49;
--rand-kraeftig: #44506180;
--text: #e5eaf1;
--text-gedaempft: #9aa7b6;
--akzent: #4d8ef7;
--akzent-text: #0d1117;
--akzent-schwach: #1d2b45;
--navi-grund: #10151c;
--navi-text: #9aa7b6;
--navi-text-aktiv: #ffffff;
--navi-aktiv: #2f6fd0;
--positiv: #46b877;
--negativ: #f0705f;
--warnung: #d9a441;
--schatten: 0 1px 2px rgba(0, 0, 0, .4);
}
}
:root[data-thema="dunkel"] {
--grund: #161b22;
--flaeche: #1c2330;
--flaeche-gedaempft: #232b39;
--rand: #303a49;
--rand-kraeftig: #445061;
--text: #e5eaf1;
--text-gedaempft: #9aa7b6;
--akzent: #4d8ef7;
--akzent-text: #0d1117;
--akzent-schwach: #1d2b45;
--navi-grund: #10151c;
--navi-text: #9aa7b6;
--navi-text-aktiv: #ffffff;
--navi-aktiv: #2f6fd0;
--positiv: #46b877;
--negativ: #f0705f;
--warnung: #d9a441;
--schatten: 0 1px 2px rgba(0, 0, 0, .4);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--grund);
color: var(--text);
font: 15px/1.5 var(--schrift);
-webkit-font-smoothing: antialiased;
}
a { color: var(--akzent); text-decoration: none; }
a:hover { text-decoration: underline; }
code {
font-family: var(--schrift-zahl);
font-size: .9em;
background: var(--flaeche-gedaempft);
padding: .1em .35em;
border-radius: 4px;
}
/* ── Grundraster ─────────────────────────────────────────────────────────── */
.rahmen {
display: grid;
grid-template-columns: 232px 1fr;
min-height: 100vh;
}
.seitenleiste {
background: var(--navi-grund);
color: var(--navi-text);
padding: 0 0 2rem;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.marke {
font-size: 1.05rem;
font-weight: 600;
color: #fff;
padding: 1.25rem 1.25rem 1rem;
letter-spacing: .01em;
}
.seitenleiste ul {
list-style: none;
margin: 0;
padding: 0;
}
.seitenleiste a {
display: flex;
align-items: center;
gap: .7rem;
padding: .6rem 1.25rem;
color: var(--navi-text);
border-left: 3px solid transparent;
font-size: .95rem;
}
.seitenleiste a:hover {
background: rgba(255, 255, 255, .05);
color: var(--navi-text-aktiv);
text-decoration: none;
}
.seitenleiste a.aktiv {
background: var(--navi-aktiv);
border-left-color: #fff;
color: var(--navi-text-aktiv);
font-weight: 500;
}
.seitenleiste .icon {
width: 1.1em;
text-align: center;
opacity: .85;
}
.hauptbereich {
display: flex;
flex-direction: column;
min-width: 0;
}
/* ── Werkzeugleiste ──────────────────────────────────────────────────────── */
.werkzeugleiste {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: .85rem 1.5rem;
background: var(--flaeche);
border-bottom: 1px solid var(--rand);
position: sticky;
top: 0;
z-index: 10;
}
.werkzeugleiste h1 {
margin: 0;
font-size: 1.35rem;
font-weight: 600;
color: var(--akzent);
}
.werkzeuge {
display: flex;
align-items: center;
gap: .75rem;
}
.werkzeug {
background: none;
border: 1px solid transparent;
color: var(--text-gedaempft);
font: inherit;
font-size: .9rem;
padding: .35rem .6rem;
border-radius: var(--radius);
cursor: pointer;
}
.werkzeug:hover {
background: var(--flaeche-gedaempft);
border-color: var(--rand);
color: var(--text);
text-decoration: none;
}
.benutzer {
font-size: .9rem;
color: var(--text-gedaempft);
}
form.inline { display: inline; margin: 0; }
.inhalt {
padding: 1.25rem 1.5rem 3rem;
flex: 1;
min-width: 0;
}
/* ── Tab-Leiste ──────────────────────────────────────────────────────────── */
.tableiste {
display: flex;
gap: .25rem;
border-bottom: 1px solid var(--rand);
margin-bottom: 1.25rem;
overflow-x: auto;
}
.tab {
padding: .5rem .9rem;
border: 1px solid transparent;
border-bottom: none;
border-radius: var(--radius) var(--radius) 0 0;
color: var(--text-gedaempft);
font-size: .93rem;
white-space: nowrap;
}
.tab:hover { background: var(--flaeche-gedaempft); text-decoration: none; }
.tab.aktiv {
background: var(--flaeche);
border-color: var(--rand);
color: var(--text);
font-weight: 500;
margin-bottom: -1px;
padding-bottom: calc(.5rem + 1px);
}
/* ── Kacheln ─────────────────────────────────────────────────────────────── */
.kachelraster {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1rem;
}
.kachel {
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: var(--radius);
box-shadow: var(--schatten);
padding: 1rem 1.15rem 1.25rem;
}
.kachel.einzeln { max-width: 640px; }
.kachel header { margin-bottom: .6rem; }
.kachel h2 {
margin: 0;
font-size: 1rem;
font-weight: 600;
}
.phasenmarke {
display: inline-block;
background: var(--akzent-schwach);
color: var(--akzent);
font-size: .8rem;
font-weight: 600;
padding: .15rem .5rem;
border-radius: 999px;
margin: 0 0 .75rem;
}
.platzhalter {
color: var(--text-gedaempft);
font-size: .92rem;
margin: .5rem 0 0;
}
.checkliste {
list-style: none;
margin: 0;
padding: 0;
font-size: .92rem;
}
.checkliste li {
padding: .3rem 0 .3rem 1.5rem;
position: relative;
color: var(--text-gedaempft);
}
.checkliste li::before {
content: "○";
position: absolute;
left: 0;
}
.checkliste li.erledigt { color: var(--text); }
.checkliste li.erledigt::before { content: "●"; color: var(--positiv); }
/* ── Tabellen ────────────────────────────────────────────────────────────── */
table.liste {
width: 100%;
border-collapse: collapse;
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: var(--radius);
overflow: hidden;
font-size: .92rem;
}
table.liste th,
table.liste td {
padding: .45rem .7rem;
text-align: left;
border-bottom: 1px solid var(--rand);
}
table.liste thead th {
background: var(--flaeche-gedaempft);
font-weight: 600;
color: var(--text-gedaempft);
white-space: nowrap;
}
table.liste tbody tr:last-child td { border-bottom: none; }
table.liste tbody tr:hover { background: var(--flaeche-gedaempft); }
.rechts { text-align: right; }
.betrag {
font-family: var(--schrift-zahl);
font-variant-numeric: tabular-nums;
text-align: right;
white-space: nowrap;
}
.betrag.negativ { color: var(--negativ); }
.nowrap { white-space: nowrap; }
/* Gliederung der Kontenübersicht: Gruppenkopf, Gruppensumme, Gesamtsumme. */
table.liste tr.gruppenzeile th {
background: var(--flaeche-gedaempft);
border-top: 1px solid var(--rand);
font-size: .85rem;
letter-spacing: .03em;
text-transform: uppercase;
color: var(--text-gedaempft);
}
table.liste tr.gruppensumme td {
font-weight: 600;
background: color-mix(in srgb, var(--flaeche-gedaempft) 55%, var(--flaeche));
}
table.liste tfoot tr.summenzeile td {
font-weight: 700;
border-top: 2px solid var(--rand-kraeftig);
background: var(--flaeche-gedaempft);
}
table.liste tr.inaktiv td { color: var(--text-gedaempft); }
table.liste input[type="text"],
table.liste input[type="number"] {
width: 100%;
padding: .25rem .4rem;
border: 1px solid var(--rand);
border-radius: 4px;
background: var(--flaeche);
color: var(--text);
font: inherit;
}
table.liste input.schmal { width: 5rem; }
/* ── Budgetplaner ────────────────────────────────────────────────────────────
Zwölf Monatsspalten passen auf keinen Bildschirm: Die Tabelle scrollt in
ihrem eigenen Rahmen, die Kategoriespalte bleibt dabei stehen. */
.breittabelle {
overflow-x: auto;
margin-bottom: 1rem;
}
table.budgetplaner { min-width: 100%; }
table.budgetplaner th,
table.budgetplaner td {
padding: .3rem .5rem;
white-space: nowrap;
}
table.budgetplaner .spalte-kategorie {
position: sticky;
left: 0;
z-index: 1;
background: var(--flaeche);
min-width: 16rem;
}
table.budgetplaner thead .spalte-kategorie,
table.budgetplaner tfoot .spalte-kategorie { background: var(--flaeche-gedaempft); }
table.budgetplaner tbody tr:hover .spalte-kategorie { background: var(--flaeche-gedaempft); }
table.budgetplaner th.gruppenkopf {
text-align: center;
border-left: 1px solid var(--rand);
}
/* Der laufende Monat ist die Spalte, auf die man zuerst schaut. */
table.budgetplaner .laufend { background: color-mix(in srgb, var(--akzent) 8%, transparent); }
table.budgetplaner tr.summenzeile-kategorie td { font-weight: 600; }
table.budgetplaner input.betragsfeld {
width: 6rem;
text-align: right;
font-family: var(--schrift-zahl);
font-variant-numeric: tabular-nums;
}
table.budgetplaner td.rechts {
font-family: var(--schrift-zahl);
font-variant-numeric: tabular-nums;
}
/* Aufklappen des Kategoriebaums; ohne JavaScript bleibt alles sichtbar. */
.klappknopf {
border: none;
background: none;
color: var(--text-gedaempft);
cursor: pointer;
font: inherit;
padding: 0 .3rem 0 0;
}
.klappknopf[aria-expanded="false"] { transform: rotate(-90deg); display: inline-block; }
tr.zugeklappt { display: none; }
/* Einrückung des Kategoriebaums. Die Ebene steht als data-Attribut an der
Zeile – so bleibt die Tabelle frei von verschachtelten Elementen. Kein
style-Attribut: Die Content-Security-Policy (style-src 'self') verwirft
Inline-Styles, die Einrückung fiele stillschweigend aus. */
.baumname {
display: inline-block;
padding-left: 0;
}
.baumname[data-ebene="1"] { padding-left: 1.4rem; }
.baumname[data-ebene="2"] { padding-left: 2.8rem; }
.baumname[data-ebene="3"] { padding-left: 4.2rem; }
.baumname[data-ebene="4"] { padding-left: 5.6rem; }
.baumname[data-ebene="5"] { padding-left: 7rem; }
.baumname[data-ebene="6"] { padding-left: 8.4rem; }
.nebentext {
color: var(--text-gedaempft);
font-size: .88rem;
}
.marke {
display: inline-block;
padding: .05rem .4rem;
border: 1px solid var(--rand-kraeftig);
border-radius: 999px;
font-size: .75rem;
color: var(--text-gedaempft);
white-space: nowrap;
}
.kennzahl {
font-family: var(--schrift-zahl);
font-size: 2rem;
font-weight: 600;
margin: .2rem 0;
}
.protokoll {
margin: .4rem 0 0;
padding-left: 1.1rem;
font-size: .9rem;
}
.protokoll li.fehler { color: var(--negativ); }
/* ── Aktionsleiste über den Listen ───────────────────────────────────────── */
.seitenaktionen {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .5rem;
margin-bottom: 1rem;
}
.seitenaktionen .fuellung { flex: 1; }
.saldoanzeige {
font-size: .9rem;
color: var(--text-gedaempft);
}
.saldoanzeige strong {
margin-left: .35rem;
font-family: var(--schrift-zahl);
font-size: 1.05rem;
color: var(--text);
}
.saldoanzeige strong.negativ { color: var(--negativ); }
/* ── Kontoblatt ──────────────────────────────────────────────────────────── */
.filterleiste {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .5rem;
margin-bottom: 1rem;
padding: .6rem .75rem;
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: var(--radius);
}
.filterleiste input[type="search"],
.filterleiste input[type="date"],
.filterleiste select {
padding: .3rem .45rem;
border: 1px solid var(--rand);
border-radius: 4px;
background: var(--flaeche);
color: var(--text);
font: inherit;
font-size: .9rem;
}
.filterleiste input[type="search"] { min-width: 16rem; }
.filterleiste select { max-width: 15rem; }
.filterleiste label { font-size: .88rem; color: var(--text-gedaempft); }
table.kontoblatt { font-size: .89rem; }
table.kontoblatt td { padding: .3rem .6rem; }
table.kontoblatt tr.gegensicht td { background: color-mix(in srgb, var(--akzent) 5%, transparent); }
.gekuerzt {
max-width: 26rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.blaettern {
display: flex;
align-items: center;
gap: .75rem;
margin-top: .8rem;
}
/* Die Splittabelle sitzt im Formular und braucht schmalere Zellen. */
table.splittabelle td { padding: .3rem .4rem; }
table.splittabelle select { max-width: 14rem; width: 100%; }
table.splittabelle input.rechts { text-align: right; }
.buchungsmaske [hidden] { display: none; }
/* ── Formulare ───────────────────────────────────────────────────────────── */
.formular {
display: flex;
flex-direction: column;
gap: .35rem;
max-width: 380px;
}
.formular.breit {
max-width: 620px;
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: var(--radius);
padding: 1.25rem;
box-shadow: var(--schatten);
}
.formular label {
font-size: .9rem;
font-weight: 500;
margin-top: .6rem;
}
.formular label.ankreuzfeld {
display: flex;
align-items: center;
gap: .5rem;
font-weight: 500;
}
/* Mehrfachauswahl im Formular – etwa die Konten einer Kontengruppe. Bei vielen
Einträgen scrollt die Liste, statt das Formular auseinanderzuziehen. */
.auswahlliste {
display: flex;
flex-direction: column;
gap: .35rem;
max-height: 22rem;
overflow-y: auto;
padding: .6rem .7rem;
border: 1px solid var(--rand);
border-radius: var(--radius);
background: var(--flaeche-gedaempft);
}
.auswahlliste .ankreuzfeld { margin: 0; }
.auswahlliste .nebentext { margin-left: .35rem; }
.formular input[type="text"],
.formular input[type="password"],
.formular input[type="number"],
.formular select {
font: inherit;
padding: .5rem .6rem;
border: 1px solid var(--rand-kraeftig);
border-radius: var(--radius);
background: var(--flaeche);
color: var(--text);
width: 100%;
}
.formular input:focus-visible,
.formular select:focus-visible,
.knopf:focus-visible {
outline: 2px solid var(--akzent);
outline-offset: 1px;
}
.formular h3 {
margin: 1.5rem 0 .5rem;
font-size: .95rem;
}
.feldhinweis {
font-size: .85rem;
color: var(--text-gedaempft);
margin: .15rem 0 0;
}
.optional { font-weight: 400; color: var(--text-gedaempft); }
.knopfleiste {
display: flex;
gap: .6rem;
margin-top: 1.25rem;
}
.knopf {
display: inline-block;
font: inherit;
font-size: .93rem;
padding: .5rem 1rem;
border: 1px solid var(--rand-kraeftig);
border-radius: var(--radius);
background: var(--flaeche);
color: var(--text);
cursor: pointer;
}
.knopf:hover { background: var(--flaeche-gedaempft); text-decoration: none; }
/* Kein eigener Außenabstand: der primäre Knopf steht sonst in jeder Knopfleiste
um genau diesen Betrag tiefer als seine Nachbarn. Den Abstand nach oben setzt
allein die .knopfleiste. */
.knopf.primaer {
background: var(--akzent);
border-color: var(--akzent);
color: var(--akzent-text);
font-weight: 500;
}
.knopf.primaer:hover { filter: brightness(1.08); }
.knopf.klein {
padding: .25rem .6rem;
font-size: .85rem;
}
.knopf.gefahr {
border-color: var(--negativ);
color: var(--negativ);
background: var(--flaeche);
}
.knopf.gefahr:hover { background: color-mix(in srgb, var(--negativ) 12%, var(--flaeche)); }
.feldwert { margin: .1rem 0 .3rem; }
/* ── Meldungen ───────────────────────────────────────────────────────────── */
.meldung {
padding: .65rem .9rem;
border-radius: var(--radius);
border: 1px solid var(--rand);
margin-bottom: 1rem;
font-size: .92rem;
}
.meldung-erfolg { background: var(--akzent-schwach); border-color: var(--akzent); }
/* Erfolgsmeldungen blenden sich nach einigen Sekunden aus (app.js). */
.meldung.verblasst {
opacity: 0;
transition: opacity .4s;
}
.meldung-fehler { background: color-mix(in srgb, var(--negativ) 12%, var(--flaeche)); border-color: var(--negativ); }
.meldung-warnung { background: color-mix(in srgb, var(--warnung) 15%, var(--flaeche)); border-color: var(--warnung); }
.hinweis { color: var(--text-gedaempft); font-size: .93rem; }
/* ── Schlichter Rahmen (Anmeldung, Fehlerseiten) ─────────────────────────── */
body.schlicht {
display: grid;
place-items: center;
min-height: 100vh;
padding: 1.5rem;
}
.karte-mitte {
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: 10px;
box-shadow: var(--schatten);
padding: 2rem;
width: 100%;
max-width: 400px;
}
.marke.gross {
padding: 0 0 1.25rem;
font-size: 1.3rem;
color: var(--text);
}
/* ── Schmale Fenster ─────────────────────────────────────────────────────── */
@media (max-width: 820px) {
.rahmen { grid-template-columns: 1fr; }
.seitenleiste {
position: static;
height: auto;
padding-bottom: .5rem;
}
.seitenleiste ul {
display: flex;
overflow-x: auto;
}
.seitenleiste a {
border-left: none;
border-bottom: 3px solid transparent;
white-space: nowrap;
}
.seitenleiste a.aktiv {
border-left: none;
border-bottom-color: #fff;
}
}