Finanzmanager/public/static/app.css
fds2610-bot 78f1b91bc0 Online-Banking: Kontenzuordnung, Sammellöschen und drei Umsatz-Fehler
Kontenzuordnung als dritter Einrichtungsschritt: Welches Bankkonto gehört zu
welchem eigenen Konto? Umsätze merken sich über `bank_konto_id`, woher sie
stammen, und ziehen mit, wenn die Zuordnung erst nach einem Abruf fällt.
Abgerufene Umsätze buchen sich auf Wunsch selbst (`Services\Bankabgleich`).

Im Kontoblatt lassen sich Buchungen sammelweise löschen; Statusmeldungen liegen
jetzt über der Ansicht, statt den Inhalt springen zu lassen.

Dazu drei Fehler, die zusammen dafür sorgten, dass abgerufene Umsätze spurlos
verschwanden – der Abruf meldete sie als vorhanden, das Kontoblatt zeigte
nichts, und in der Vorschlagsliste standen sie ebenfalls nicht:

- Eine gelöschte Buchung ließ ihren Umsatz auf „uebernommen“ stehen. Der
  Fremdschlüssel setzte nur `buchung_id` auf NULL. `Buchung::loeschen()` gibt
  den Umsatz jetzt vorher frei, `Bankumsatz::verwaisteOeffnen()` fängt die Wege
  ab, die daran vorbeiführen – Kaskaden lösen in MariaDB keine Trigger aus.
- Ein Bankkonto entstand doppelt, wenn die Bank die IBAN nicht bei jedem Dialog
  mitliefert. Gesucht wird jetzt über die Kontonummer; eine nachgelieferte IBAN
  wird am vorhandenen Eintrag nachgetragen.
- Der Fingerabdruck hing an der IBAN und kippte mit ihr. Er nimmt jetzt die
  `bank_konto_id`, die fest steht, solange es den Bankzugang gibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:54:23 +02:00

1731 lines
44 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; }
/* Schiebt einen Eintrag ans andere Ende der Leiste – etwa „Kacheln wählen“, das
kein Register ist, sondern ein Weg hinaus. */
.tab.rechtsbuendig { margin-left: auto; }
.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 header.mitschalter {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem 1rem;
flex-wrap: wrap;
}
/* Der Schalter teilt sich die Zeile mit der Überschrift und bleibt oben rechts;
der erklärende Satz nimmt die ganze nächste Zeile. Stünde er in derselben
Zeile, drückte er den Schalter in einer schmalen Kachel nach unten. */
.kachel header.mitschalter > h2 { min-width: 0; }
.kachel header.mitschalter > .kachelhinweis { flex: 1 0 100%; margin: 0; }
.kachelhinweis {
margin: .25rem 0 0;
color: var(--text-gedaempft);
font-size: .85rem;
}
.kachel h2 {
margin: 0;
font-size: 1rem;
font-weight: 600;
}
/* Kleine Marke: Zustand (Monatsabschluss), Dateiname (Import) oder der Schritt
eines mehrteiligen Dialogs (Einrichtung einer Bankverbindung). */
.phasenmarke,
.schrittmarke {
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); }
/* ── Diagramme ───────────────────────────────────────────────────────────────
Selbst erzeugtes SVG, keine Diagrammbibliothek. Die Farbe eines Kontos steht
als Klasse am Element: Ein fill-Attribut ginge zwar an der CSP vorbei, könnte
aber nicht auf das dunkle Thema reagieren. Jede Serie setzt --farbe, davon
leben Balken (fill) und Legendenpunkt (background) gleichermaßen. */
.serie-1 { --farbe: #4c78a8; }
.serie-2 { --farbe: #f58518; }
.serie-3 { --farbe: #54a24b; }
.serie-4 { --farbe: #e45756; }
.serie-5 { --farbe: #72b7b2; }
.serie-6 { --farbe: #b279a2; }
.serie-7 { --farbe: #d8b02c; }
.serie-8 { --farbe: #9d755d; }
.serie-9 { --farbe: #2f8f8a; }
.serie-10 { --farbe: #8f8ad4; }
.serie-11 { --farbe: #e07b91; }
.serie-12 { --farbe: #7b8a99; }
.diagrammbereich {
display: flex;
flex-direction: column;
gap: .6rem;
}
/* Achsenbeschriftung und Zeichenfläche sind zwei SVGs nebeneinander. Die Achse
ist fest, die Fläche nimmt den Rest – und weil deren Balken waagerecht in
Prozent sitzen, rücken sie in einer breiten Kachel auseinander, statt breiter
zu werden. Ein einziges SVG könnte das nicht: Prozentangaben rechnen immer
auf die volle Breite, die feste Achse ließe sich nicht abziehen. */
.diagrammflaeche {
display: flex;
align-items: flex-start;
width: 100%;
}
.diagrammachse { flex: none; }
.diagramm {
flex: 1 1 0;
min-width: 0;
display: block;
overflow: visible;
}
.diagramm rect { fill: var(--farbe); }
.diagramm-gitter {
stroke: var(--rand);
stroke-width: 1;
}
.diagramm-nulllinie {
stroke: var(--rand-kraeftig);
stroke-width: 1.5;
}
.diagramm-achse,
.diagramm-beschriftung {
fill: var(--text-gedaempft);
font-family: var(--schrift);
font-size: 10px;
}
/* Ziffernschrift für die Achse, aber schmal genug für 21 Punkte Spaltenabstand
bleiben die Monatskürzel darunter. */
.diagramm-achse {
font-family: var(--schrift-zahl);
font-size: 11px;
}
.diagramm-beschriftung.schwach { opacity: .7; }
/* Unter der Zeichnung und in kleiner Schrift: So bleibt die Kachel so breit wie
ihre Nachbarn, und zwölf Kontonamen passen in zwei Spalten darunter. */
.diagrammlegende {
list-style: none;
margin: 0;
padding: 0;
align-self: stretch;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
gap: 0 1rem;
font-size: .74rem;
align-content: start;
}
.diagrammlegende li {
display: flex;
align-items: center;
gap: .4rem;
padding: .1rem 0;
}
.legendenfarbe {
width: .65rem;
height: .65rem;
border-radius: 2px;
background: var(--farbe);
flex: none;
}
.legendenname {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* „Übrige Konten“ führt eine Ebene tiefer: Der Eintrag ist ein Link und sieht
auch danach aus – sonst probierte ihn niemand aus. */
.legendenname.tiefer {
color: var(--akzent);
text-decoration: underline dotted;
text-underline-offset: 2px;
}
.legendenname.tiefer:hover { text-decoration: underline; }
.diagrammpfad {
margin: 0;
font-size: .74rem;
color: var(--text-gedaempft);
}
.kachelfuss { margin-top: 1rem; }
/* Einnahmen und Ausgaben tragen keine Seriennummer, sondern die Bedeutungsfarben
der Anwendung – grün und rot lesen sich hier ohne Legende. */
.balken-einnahme { --farbe: var(--positiv); }
.balken-ausgabe { --farbe: var(--negativ); }
.saldofarbe { --farbe: var(--akzent); }
.diagramm-saldo {
stroke: var(--akzent);
stroke-width: 1.5;
fill: none;
}
.diagramm circle.diagramm-saldo-punkt { fill: var(--akzent); }
/* ── Ringdiagramm ──────────────────────────────────────────────────────────── */
.ringbereich {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.ringdiagramm {
flex: 0 0 auto;
width: 160px;
height: 160px;
}
/* Der schmale Rand in Flächenfarbe trennt benachbarte Segmente, ohne dass es
nach Umrandung aussieht. */
.ringdiagramm path {
fill: var(--farbe);
stroke: var(--flaeche);
stroke-width: 1.5;
}
.ringsumme {
fill: var(--text);
font-family: var(--schrift-zahl);
font-size: 13px;
font-weight: 600;
text-anchor: middle;
}
.ringtext {
fill: var(--text-gedaempft);
font-family: var(--schrift);
font-size: 10px;
}
.ringlegende {
list-style: none;
margin: 0;
padding: 0;
flex: 1 1 12rem;
min-width: 0;
font-size: .78rem;
}
.ringlegende li {
display: grid;
grid-template-columns: .65rem 1fr auto auto;
align-items: center;
gap: .45rem;
padding: .15rem 0;
}
.ringlegende .legendenbetrag {
font-family: var(--schrift-zahl);
font-size: .74rem;
}
.ringlegende .legendenanteil {
color: var(--text-gedaempft);
font-size: .72rem;
min-width: 2.4rem;
text-align: right;
}
/* ── Offene Vorgänge ───────────────────────────────────────────────────────── */
.vorgangsliste {
list-style: none;
margin: 0;
padding: 0;
font-size: .92rem;
}
.vorgangsliste li { padding: .3rem 0; border-bottom: 1px solid var(--rand); }
.vorgangsliste li:last-child { border-bottom: none; }
.vorgangszahl {
display: inline-block;
min-width: 1.6rem;
padding: .05rem .4rem;
margin-right: .4rem;
border-radius: 999px;
background: var(--akzent-schwach);
color: var(--akzent);
font-family: var(--schrift-zahl);
font-size: .8rem;
text-align: center;
}
/* ── Ein/Aus-Schalter ────────────────────────────────────────────────────────
Die Bahn mit dem laufenden Knopf, wie man ihn von Mobilgeräten kennt. Ein
abschickender Knopf statt eines Ankreuzfelds: Ohne Inline-Skript könnte ein
Ankreuzfeld sein Formular nicht selbst abschicken, und Inline-Skripte
verbietet die Content-Security-Policy. */
.schalterform { margin: 0; }
.schalter {
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .15rem;
border: none;
border-radius: var(--radius);
background: none;
color: var(--text-gedaempft);
font: inherit;
font-size: .85rem;
cursor: pointer;
}
.schalter[aria-checked="true"] { color: var(--text); }
.schalter:focus-visible {
outline: 2px solid var(--akzent);
outline-offset: 1px;
}
.schalterbahn {
position: relative;
flex: none;
width: 2.3rem;
height: 1.25rem;
border-radius: 999px;
background: var(--rand-kraeftig);
transition: background .15s ease;
}
.schalterknopf {
position: absolute;
top: .15rem;
left: .15rem;
width: .95rem;
height: .95rem;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 2px rgb(0 0 0 / 35%);
transition: transform .15s ease;
}
.schalter[aria-checked="true"] .schalterbahn { background: var(--akzent); }
.schalter[aria-checked="true"] .schalterknopf { transform: translateX(1.05rem); }
/* ── 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); }
/* Spalte mit dem Ankreuzfeld der Mehrfachauswahl – so schmal wie möglich. */
table.liste th.wahlspalte,
table.liste td.wahlspalte {
width: 1px;
padding-right: .3rem;
text-align: center;
}
/* Die Zuordnung Bankkonto → eigenes Konto, kurz im Zugangsformular. */
ul.kontenzuordnung {
margin: .25rem 0 .75rem;
padding-left: 1.1rem;
}
ul.kontenzuordnung li { margin-bottom: .2rem; }
/* Zusatzzeile einer Liste – die Felder für ein neu anzulegendes Konto in der
Kontenzuordnung. Sie hängt optisch an der Zeile darüber. */
table.liste tr.unterzeile td {
border-top: 0;
padding-top: 0;
}
table.liste tr[hidden] { display: none; }
td.neues-konto {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .4rem .6rem;
}
td.neues-konto label {
margin: 0;
font-size: .85rem;
color: var(--text-gedaempft);
}
td.neues-konto input[type="text"] { width: auto; min-width: 12rem; }
td.neues-konto .nebentext { flex-basis: 100%; }
/* Konten mit unkategorisierten Buchungen aus dem laufenden Jahr. Fett wie im
FinanzManager – kein Farbwechsel, sonst kollidierte er mit der Ampel der
Salden in derselben Zeile. */
table.liste.konten tr.unkategorisiert td:first-child a { font-weight: 700; }
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; }
/* Breit genug für „123.456,78“ – eine Stelle mehr, als der Haushalt braucht. */
table.budgetplaner input.betragsfeld {
width: 7rem;
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;
}
/* Zahnrad vor jedem Eingabefeld: verteilt den Wert über die Zeile. Das Menü
dazu gibt es nur einmal; app.js hängt es in die angeklickte Zelle, die es
deshalb verankern muss. Positioniert wird über CSS, nicht über ein per
JavaScript gesetztes style-Attribut – das verwürfe die
Content-Security-Policy (style-src 'self'). */
table.budgetplaner td.budgetzelle { position: relative; }
.zahnrad {
border: none;
background: none;
color: var(--text-gedaempft);
cursor: pointer;
font-size: .85rem;
line-height: 1;
/* Ein Drittel des früheren Abstands: Der kam aus diesem Padding plus dem
Leerzeichen im Markup, das die Zelle in Ziffernschrift breit setzte. */
padding: 0 .27rem 0 0;
}
.zahnrad:hover,
.zahnrad[aria-expanded="true"] { color: var(--akzent); }
.zahnradmenue {
position: absolute;
top: 100%;
right: .3rem;
z-index: 3;
display: flex;
flex-direction: column;
min-width: 10rem;
padding: .2rem;
border: 1px solid var(--rand-kraeftig);
border-radius: var(--radius);
background: var(--flaeche);
box-shadow: 0 4px 12px rgb(0 0 0 / 18%);
}
/* Fließtext, nicht Zahlensatz: Die Zelle vererbt sonst ihre Ziffernschrift. */
.menueeintrag {
border: none;
background: none;
color: var(--text);
cursor: pointer;
font: inherit;
font-family: var(--schrift);
font-size: .88rem;
text-align: left;
padding: .35rem .5rem;
border-radius: var(--radius);
white-space: nowrap;
}
.menueeintrag:hover { background: var(--flaeche-gedaempft); }
/* 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;
}
/* Der Hinweis hinter einem Ankreuzfeld statt darunter – bei einer Liste
gleichartiger Schalter hielte eine eigene Zeile je Hinweis sie zu weit
auseinander. */
.feldhinweis.inline {
margin: 0;
font-weight: 400;
}
.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;
}
/* Ein Knopf, der die gerade gewählte Ansicht markiert – etwa die drei
Spaltenbilder des Budgetplaners. */
.knopf.aktiv {
background: var(--akzent-schwach);
border-color: var(--akzent);
font-weight: 500;
}
.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)); }
/* Gesperrt, solange nichts ausgewählt ist – sichtbar, aber ohne Aufforderung. */
.knopf:disabled,
.knopf:disabled:hover {
opacity: .5;
cursor: default;
background: var(--flaeche);
}
.feldwert { margin: .1rem 0 .3rem; }
/* ── Meldungen ───────────────────────────────────────────────────────────── */
/* Die Meldungen liegen über der Ansicht, nicht in ihr: Sie schieben sich von
ganz oben ins Bild und wieder hinaus, ohne den Inhalt zu verschieben – sonst
rückt die halbe Seite auf und beim Ausblenden wieder zurück. */
.meldungen {
position: fixed;
top: 0;
left: 50%;
transform: translateX(-50%);
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
gap: .4rem;
max-width: 80vw;
padding-top: .5rem;
pointer-events: none;
}
.meldung {
padding: .55rem .9rem;
border-radius: var(--radius);
border: 1px solid var(--rand);
background: var(--flaeche);
box-shadow: var(--schatten);
font-size: .92rem;
/* So schmal wie die Meldung lang ist; erst an der 80-%-Grenze des
Elternelements wird umgebrochen. */
max-width: 100%;
overflow-wrap: anywhere;
/* Anklicken blendet aus (app.js) – sonst verdeckte eine stehenbleibende
Fehlermeldung die Werkzeugleiste bis zum nächsten Seitenwechsel. */
pointer-events: auto;
cursor: pointer;
animation: meldung-herein .3s ease-out;
}
@keyframes meldung-herein {
from { transform: translateY(-150%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.meldung-erfolg { background: var(--akzent-schwach); border-color: var(--akzent); }
/* Erfolgsmeldungen schieben sich nach einigen Sekunden wieder hinaus (app.js). */
.meldung.verblasst {
opacity: 0;
transform: translateY(-150%);
transition: opacity .4s, transform .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); }
@media (prefers-reduced-motion: reduce) {
.meldung { animation: none; }
.meldung.verblasst { transform: none; transition: opacity .4s; }
}
.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);
}
/* ── Auswertungen ────────────────────────────────────────────────────────── */
/* Aufklappmenü ohne JavaScript: <details> in der Aktionsleiste. Ein eigenes
Menü mit Skript wäre mehr Code für dasselbe Verhalten – und die
Content-Security-Policy verbietet Inline-Handler ohnehin. */
.menue { position: relative; display: inline-block; }
.menue > summary {
list-style: none;
user-select: none;
}
.menue > summary::-webkit-details-marker { display: none; }
.menueliste {
position: absolute;
z-index: 20;
top: calc(100% + .3rem);
left: 0;
min-width: 14rem;
padding: .35rem;
background: var(--flaeche);
border: 1px solid var(--rand-kraeftig);
border-radius: var(--radius);
box-shadow: var(--schatten);
}
.menueliste.breit { min-width: 22rem; padding: .9rem; }
.menueliste > a {
display: block;
padding: .4rem .55rem;
border-radius: 4px;
color: var(--text);
font-size: .92rem;
}
.menueliste > a:hover { background: var(--flaeche-gedaempft); text-decoration: none; }
.speichernform {
display: flex;
flex-direction: column;
gap: .2rem;
}
.speichernform label { font-size: .88rem; font-weight: 500; margin-top: .5rem; }
.speichernform input[type="text"],
.speichernform select {
font: inherit;
font-size: .92rem;
padding: .4rem .5rem;
border: 1px solid var(--rand-kraeftig);
border-radius: 4px;
background: var(--flaeche);
color: var(--text);
width: 100%;
}
.speichernform .knopfleiste { margin-top: .9rem; }
/* Die Berichtsliste einer Gruppenkachel. */
.berichtsliste {
list-style: none;
margin: .3rem 0 0;
padding: 0;
}
.berichtsliste li {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: .5rem;
padding: .25rem 0;
border-bottom: 1px solid var(--rand);
}
.berichtsliste li:last-child { border-bottom: none; }
.berichtssymbol { color: var(--text-gedaempft); margin-right: .35rem; }
.berichtstyp {
color: var(--text-gedaempft);
font-size: .82rem;
white-space: nowrap;
}
/* Die Filterleiste des Berichts steht mehrzeilig – anders als die einzeilige
des Kontoblatts, von der sie das Aussehen erbt. */
.berichtsfilter {
flex-direction: column;
align-items: stretch;
gap: .6rem;
}
.filterzeile {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: .5rem .9rem;
}
.filterzeile > label {
display: flex;
flex-direction: column;
gap: .15rem;
font-size: .82rem;
color: var(--text-gedaempft);
}
.filterzeile label.ankreuz {
flex-direction: row;
align-items: center;
gap: .35rem;
padding-bottom: .35rem;
}
.filterzeile select[multiple] {
min-width: 13rem;
max-width: 18rem;
font-size: .88rem;
/* Vorformatierter Text: Nur so bleibt die Einrückung des Kategoriebaums in
den Optionen stehen – der Browser faltet Leerzeichen sonst zusammen. */
white-space: pre;
}
.einschraenkung > summary {
cursor: pointer;
font-size: .85rem;
color: var(--text-gedaempft);
padding: .2rem 0;
}
.einschraenkung[open] > summary { margin-bottom: .5rem; }
.hinweis.klein { font-size: .82rem; margin-top: .6rem; }
.berichtskopf {
text-align: center;
margin: 1.4rem 0 .8rem;
}
.berichtskopf h2 { margin: 0; font-size: 1.15rem; }
.berichtskopf p {
margin: .15rem 0 0;
color: var(--text-gedaempft);
font-size: .88rem;
}
table.bericht { font-size: .9rem; }
table.bericht td { padding: .28rem .7rem; }
/* Abschnittsüberschrift „Einnahmen“ / „Ausgaben“. */
table.bericht tr.abschnittszeile th {
text-align: left;
padding-top: .8rem;
background: var(--flaeche-gedaempft);
font-size: .95rem;
}
/* „Gesamt 400 - Leben“ – die Summe eines Kategorieastes. */
table.bericht tr.astsumme td {
font-weight: 600;
background: color-mix(in srgb, var(--akzent) 6%, transparent);
}
table.bericht tr.ohnezuordnung td { color: var(--text-gedaempft); font-style: italic; }
/* Zeilen- und Wertlinks führen in den Drilldown. Sie sollen die Tabelle nicht
blau einfärben – erst beim Überfahren zeigt sich, dass es weitergeht. */
table.bericht .zeilenlink,
table.bericht .wertlink {
color: inherit;
}
table.bericht .zeilenlink:hover,
table.bericht .wertlink:hover {
color: var(--akzent);
text-decoration: underline;
}
/* Zeilenweise Formulare: Name, Sortierung und Knopf nebeneinander. */
.zeilenform {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .5rem;
margin: 0;
}
.zeilenform label {
display: flex;
align-items: center;
gap: .35rem;
font-size: .88rem;
}
.zeilenform input[type="text"],
.zeilenform input[type="number"] {
font: inherit;
font-size: .92rem;
padding: .35rem .5rem;
border: 1px solid var(--rand-kraeftig);
border-radius: 4px;
background: var(--flaeche);
color: var(--text);
}
.zeilenform input.schmal { width: 5rem; }
/* ── Online-Banking ──────────────────────────────────────────────────────────
Die Abgleichliste ist keine Tabelle: Zu jedem Umsatz gehören mehrere Wege
(verbinden, neu buchen, verwerfen), jeder mit eigenem Formular. In Zellen
gepresst wäre das unlesbar; hier bekommt jeder Umsatz eine eigene Karte. */
.umsatz {
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: var(--radius);
padding: .75rem .9rem;
margin-bottom: .6rem;
}
.umsatzkopf {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.umsatztext { min-width: 0; }
.umsatztext .nebentext { display: block; font-size: .8rem; }
.verwendungszweck {
margin: .3rem 0 0;
font-size: .88rem;
color: var(--text-gedaempft);
overflow-wrap: anywhere;
}
.umsatzbetrag {
font-family: var(--schrift-zahl);
font-size: 1.05rem;
font-weight: 600;
white-space: nowrap;
}
.umsatzaktion {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: .4rem .6rem;
margin-top: .5rem;
padding-top: .5rem;
border-top: 1px dashed var(--rand);
}
.umsatzaktion select { max-width: 16rem; }
.vorschlagsmarke {
background: var(--akzent-schwach);
color: var(--akzent);
font-size: .75rem;
font-weight: 600;
padding: .1rem .5rem;
border-radius: 999px;
white-space: nowrap;
}
/* Statusmarken der Abrufliste. Sie erben von .marke und färben nur den Rand –
ein voll gefüllter Punkt zöge in einer Tabelle zu viel Aufmerksamkeit. */
.marke-erfolg { color: var(--positiv); border-color: var(--positiv); }
.marke-fehler { color: var(--negativ); border-color: var(--negativ); }
.marke-tan_offen { color: var(--warnung); border-color: var(--warnung); }
.marke-abgebrochen { color: var(--text-gedaempft); }
.tanhinweis {
background: var(--flaeche-gedaempft);
border-left: 3px solid var(--akzent);
padding: .5rem .7rem;
margin: .5rem 0;
font-size: .93rem;
}
/* photoTAN-Grafiken liefern die Banken klein; unskaliert wären sie kaum zu
scannen, größer als die Kachel dürfen sie aber auch nicht werden. */
.tanbild {
display: block;
max-width: 100%;
width: 240px;
height: auto;
margin: .75rem 0;
image-rendering: pixelated;
background: #fff;
padding: .5rem;
border-radius: var(--radius);
}
.protokolltext {
margin: 0;
padding: .6rem .7rem;
background: var(--flaeche-gedaempft);
border-radius: var(--radius);
font-family: var(--schrift-zahl);
font-size: .78rem;
white-space: pre-wrap;
overflow-x: auto;
}
.feldpaar {
display: flex;
gap: .5rem;
}
.regeltabelle select,
.regeltabelle input[type="text"] { max-width: 12rem; }
/* ── Wertpapiere ─────────────────────────────────────────────────────────── */
/* Die Kennzahlen über der Depotübersicht: Wert, Einstand, Gewinn, Erträge –
nebeneinander, aber umbrechend, damit sie auch auf schmalen Fenstern lesbar
bleiben. */
.kennzahlenleiste {
display: flex;
flex-wrap: wrap;
gap: .75rem;
margin-bottom: 1.25rem;
}
.kennzahlkachel {
flex: 1 1 9rem;
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: var(--radius);
padding: .6rem .8rem;
display: flex;
flex-direction: column;
gap: .1rem;
}
.kennzahltitel {
font-size: .78rem;
color: var(--text-gedaempft);
}
.kennzahlkachel .kennzahl {
font-size: 1.25rem;
margin: 0;
}
/* Zahlenlastige Tabellen bekommen Ziffernschrift in den rechtsbündigen
Spalten – so stehen Kommastellen untereinander. */
table.liste.zahlen td.rechts { font-family: var(--schrift-zahl); }
.wertflaeche {
fill: var(--akzent);
opacity: .18;
stroke: none;
}
.wertlinie {
fill: none;
stroke: var(--akzent);
stroke-width: 2;
}
/* Der Einstand als gestrichelte Linie: Er ist die Vergleichsgröße, nicht der
Hauptverlauf. */
.einstandslinie {
fill: none;
stroke: var(--text-gedaempft);
stroke-width: 1.5;
stroke-dasharray: 4 3;
}
.diagramm circle.wertpunkt { fill: var(--akzent); }
.wertfarbe { --farbe: var(--akzent); }
.einstandsfarbe { --farbe: var(--text-gedaempft); }
/* ── Termine, Verträge, Sparziele ────────────────────────────────────────── */
/* Was überfällig ist, wird nicht rot eingefärbt, sondern bekommt eine Kante:
Eine ganze Zeile in Signalfarbe liest sich schlechter, nicht dringender. */
table.liste tr.ueberfaellig td:first-child {
border-left: 3px solid var(--negativ);
padding-left: calc(.6rem - 3px);
}
.kalendermonat { margin-bottom: 1.5rem; }
.kalendermonat h3 {
margin: 0 0 .4rem;
font-size: .95rem;
color: var(--text-gedaempft);
}
.kalendertag {
width: 3.5rem;
white-space: nowrap;
}
.tagzahl {
font-family: var(--schrift-zahl);
font-size: 1.1rem;
font-weight: 600;
margin-right: .3rem;
}
table.liste tr.luecke td {
text-align: center;
font-style: italic;
background: var(--flaeche-gedaempft);
}
/* <progress> statt eines eigenen Balkens: Die Breite käme sonst nur über ein
style-Attribut zustande, und das verbietet die Content-Security-Policy. */
.fortschritt {
display: block;
width: 100%;
height: .75rem;
margin: .5rem 0 .3rem;
border: none;
border-radius: 999px;
background: var(--flaeche-gedaempft);
appearance: none;
}
.fortschritt::-webkit-progress-bar {
background: var(--flaeche-gedaempft);
border-radius: 999px;
}
.fortschritt::-webkit-progress-value {
background: var(--positiv);
border-radius: 999px;
}
.fortschritt::-moz-progress-bar {
background: var(--positiv);
border-radius: 999px;
}
.fortschrittstext {
margin: 0 0 .5rem;
font-size: .95rem;
}
.marke-entwurf { color: var(--warnung); border-color: var(--warnung); }
.marke-gesendet { color: var(--positiv); border-color: var(--positiv); }
.marke-storniert { color: var(--text-gedaempft); }
.kennzahl.klein { font-size: 1rem; }
/* ── 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;
}
}