Files
certain/web/src/finance.css
T
ScarriffleandClaude Opus 5 76dbd3b481 feat: Geld, an das man nicht herankommt, zaehlt nicht mehr als verfuegbar
Eine Saeule 3a ist gesperrt – bis zur Rente, zur Selbstaendigkeit oder
zur Auswanderung. Bisher zaehlte ihr Stand in dieselbe Summe wie das
Girokonto, und die Antwort auf "wie viel habe ich" war um genau diesen
Betrag zu hoch. Das Geld steht da und ist doch nicht da.

Konten lassen sich jetzt als gebunden kennzeichnen. "Gesamt" zeigt dann
nur noch, was wirklich ausgegeben werden kann; darunter steht
"zusaetzlich CHF 2'004.61 gebunden" – aufklappbar, mit jedem Konto
einzeln. Eine Summe, die man nicht nachrechnen kann, muss man glauben.

Verschwiegen wird nichts: Der Saldo bleibt am Konto stehen, und
Vermoegensverlauf wie Nettovermoegen rechnen weiter damit. Es nicht
ausgeben zu koennen heisst nicht, es nicht zu besitzen – das ist der
ganze Unterschied, und ein Test haelt ihn fest.

Ein eigenes Kennzeichen, keine Ableitung aus dem Kontotyp: Ein frei
verkaeufliches Depot und eine gesperrte Saeule 3a kommen beide als
"depot" herein. Am Typ laesst sich das nicht ablesen – am
ausdruecklichen Eintrag schon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 20:29:34 +02:00

2008 lines
48 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.
/* Eigene Stil-Ergänzungen für Certain Finance – bewusst getrennt von der aus
der Vorlage übernommenen styles.css, damit Basis und Ergänzung sauber bleiben. */
.brand-mark {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.brand-logo {
display: block;
flex: 0 0 auto;
}
.brand-name {
font-weight: 650;
font-size: 1rem;
letter-spacing: -0.01em;
color: var(--text);
}
/* Beträge: Vorzeichenfarbe. Grün = Einnahme/im Budget, Rot = Ausgabe/über Budget. */
.amount.pos { color: var(--ok); }
.amount.neg { color: var(--danger); }
.amount.muted { color: var(--muted); }
/* Umgerechneter Zweitbetrag (Originalwährung -> Hauptwährung) klein und gedämpft. */
.amount-converted {
color: var(--muted);
font-size: 0.82em;
margin-left: 4px;
}
/* Schlichtes zentriertes Layout für Login / Passwort ändern. */
.auth-screen {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--sp-5);
background: var(--bg);
}
.auth-card {
width: 100%;
max-width: 380px;
}
.auth-card .brand-mark {
justify-content: center;
margin-bottom: var(--sp-4);
}
/* Eingabefeld mit angehängtem Icon-Knopf (z. B. Passwort ein-/ausblenden). */
.input-with-button {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.input-with-button > input {
flex: 1 1 auto;
min-width: 0;
}
/* Flex-Kinder dürfen schrumpfen – sonst drängt ein 100%-breites select/input
ein danebenliegendes Feld auf 0 (das war der Grund für das winzige IBAN-Feld). */
.row > * {
min-width: 0;
}
/* Leerer Zustand (z. B. Dashboard ohne Karten). */
.empty-state {
text-align: center;
color: var(--muted);
padding: var(--sp-6) var(--sp-4);
}
.empty-state .icon {
opacity: 0.5;
margin-bottom: var(--sp-3);
}
/* Farbiges Icon-Plättchen (Konten/Kategorien). */
.icon-chip {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
flex: 0 0 auto;
}
/* Betrag + umgerechneter Wert (rechtsbündig gestapelt). */
.money {
display: inline-flex;
flex-direction: column;
align-items: flex-end;
line-height: 1.3;
}
.money .amount {
font-variant-numeric: tabular-nums;
font-weight: 560;
}
/* Icon-Auswahlgitter. */
.icon-picker {
display: flex;
flex-wrap: wrap;
gap: var(--sp-2);
margin-top: 5px;
}
.icon-picker-item {
width: 38px;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--muted);
cursor: pointer;
transition: border-color .12s, color .12s, background .12s;
}
.icon-picker-item:hover {
color: var(--text);
background: var(--surface-2);
}
.icon-picker-item.selected {
background: var(--accent-soft);
}
/* Farbwähler-Zeile. */
.color-row {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-top: 5px;
}
.color-row input[type="color"] {
width: 44px;
height: 34px;
padding: 2px;
cursor: pointer;
}
/* Name + Icon nebeneinander (Listen). */
.named {
display: flex;
align-items: center;
gap: var(--sp-3);
min-width: 0;
}
.named .named-text {
min-width: 0;
}
.named .named-sub {
color: var(--muted);
font-size: 0.8rem;
}
/* Kategorien-Verwaltung: durchgehende Liste in EINER Karte, die Gruppen nur als
dezente Zwischenüberschrift dazwischen (keine abgesetzten Gruppen-Karten). */
.cat-section + .cat-section {
margin-top: var(--sp-5);
}
.cat-section-head {
display: flex;
align-items: center;
gap: var(--sp-2);
padding-bottom: var(--sp-2);
margin-bottom: 2px;
border-bottom: 2px solid var(--border-strong);
}
.cat-section-name {
flex: 1;
min-width: 0;
font-size: 1.05rem;
font-weight: 650;
}
.cat-row {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: 9px 4px;
border-bottom: 1px solid var(--border);
}
.cat-section .cat-row:last-child {
border-bottom: none;
}
.cat-row:hover {
background: var(--surface-2);
}
.cat-row .grow {
flex: 1;
min-width: 0;
}
/* Seitenkopf mit Titel + Aktion rechts. */
.page-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-3);
margin-bottom: var(--sp-4);
}
.page-head h1 {
margin: 0;
}
/* Konten. */
.account-total {
display: flex;
align-items: baseline;
justify-content: space-between;
padding-bottom: var(--sp-3);
margin-bottom: var(--sp-2);
border-bottom: 1px solid var(--border);
}
.account-total strong {
font-size: 1.2rem;
font-variant-numeric: tabular-nums;
}
.account-list {
display: flex;
flex-direction: column;
}
.account-row {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3) 0;
border-bottom: 1px solid var(--border);
}
.account-row:last-child {
border-bottom: none;
}
/* Die Kategorie-Auswahl ist `width: 100%` – als Flex-Element heisst das „ich
brauche die ganze Zeile", und alles daneben wird bis zum Umbruch
zusammengedrückt. In einer Listenzeile bekommt sie deshalb eine eigene
Breite, die nur noch schrumpfen darf. */
.account-row .catsel {
flex: 0 1 clamp(180px, 26%, 300px);
width: auto;
/* Ohne das schrumpft ein Flex-Element nie unter die Breite seines längsten
Wortes – die Auswahl bliebe breiter als vorgesehen und schöbe die Knöpfe
aus der Zeile. Der Text darin kürzt sich selbst mit „…". */
min-width: 0;
}
/* Der Abstand darüber ist für Formulare gedacht, wo die Auswahl unter ihrer
Beschriftung steht. In einer mittig ausgerichteten Zeile verrutscht sie nur. */
.account-row .catsel-btn {
margin-top: 0;
}
/* Beträge in einer Spalte statt ausgefranst. */
.account-row .money {
flex: 0 0 auto;
min-width: 104px;
white-space: nowrap;
}
/* Der Name nimmt den übrigen Platz. Nicht mit „…" abschneiden: Die Texte
sollen sich weiterhin vollständig markieren und kopieren lassen. */
.account-row .named-text {
flex: 1 1 auto;
min-width: 0;
}
@media (max-width: 820px) {
/* Auf dem Telefon passt das nicht in eine Zeile. Zwei Zeilen: oben Name und
Betrag, darunter Auswahl und Knöpfe. Die Prozentwerte sind die Bruchstelle
– sie sind gross genug, dass unten kein Platz mehr für Name und Betrag
bleibt, und klein genug, dass die Knöpfe neben der Auswahl bleiben. */
.account-row {
flex-wrap: wrap;
row-gap: var(--sp-2);
}
.account-row .named-text {
flex: 1 1 50%;
}
.account-row .money {
min-width: 0;
}
.account-row .catsel {
flex: 1 1 40%;
}
}
.row-actions {
display: flex;
gap: 4px;
flex: 0 0 auto;
}
/* Filterleiste. */
.filter-bar {
display: flex;
gap: var(--sp-3);
flex-wrap: wrap;
margin-bottom: var(--sp-4);
}
.filter-bar select {
width: auto;
min-width: 160px;
margin-top: 0;
}
/* Segmentierte Umschaltung (Ausgabe/Einnahme/Transfer). */
.seg {
display: flex;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
overflow: hidden;
margin-bottom: var(--sp-4);
}
.seg-btn {
flex: 1;
padding: 8px 10px;
border: none;
background: var(--surface);
color: var(--muted);
font-weight: 560;
cursor: pointer;
border-right: 1px solid var(--border);
}
.seg-btn:last-child {
border-right: none;
}
.seg-btn.active {
background: var(--accent);
color: #fff;
}
.nowrap {
white-space: nowrap;
}
/* Nimmt den freien Platz in einer Flex-Zeile und darf schrumpfen (Ellipsis). */
.grow {
flex: 1 1 auto;
min-width: 0;
}
/* Nutzer-Chip in der Seitenleisten-Fußzeile ist ein Link auf „Mein Konto". */
a.sidebar-user {
text-decoration: none;
border-radius: var(--radius-sm);
padding: 2px 4px;
margin: -2px -4px;
transition: background .12s;
}
a.sidebar-user:hover {
background: var(--surface-2);
}
a.sidebar-user:hover .name {
color: var(--accent);
}
/* Budget: mehrere Monate nebeneinander. */
.budget-months {
display: grid;
gap: var(--sp-4);
grid-template-columns: repeat(var(--cols, 1), minmax(0, 1fr));
align-items: start;
}
@media (max-width: 820px) {
.budget-months {
grid-template-columns: 1fr;
}
}
.budget-col {
min-width: 0;
}
.budget-col-head {
display: flex;
align-items: center;
gap: 4px;
padding-bottom: var(--sp-2);
margin-bottom: var(--sp-2);
border-bottom: 2px solid var(--border-strong);
}
.budget-col-head strong {
font-size: 1.05rem;
}
.budget-col-income {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 4px 0 var(--sp-2);
margin-bottom: var(--sp-2);
border-bottom: 1px solid var(--border);
}
.budget-crow {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 6px 0;
border-bottom: 1px solid var(--border);
}
.budget-crow:last-child {
border-bottom: none;
}
.budget-crow .amount.small {
flex: 0 0 auto;
}
/* Budget. */
.budget-navbar {
display: flex;
align-items: center;
gap: var(--sp-3);
}
.budget-month {
font-size: 1.05rem;
min-width: 170px;
text-align: center;
}
.flip {
transform: rotate(180deg);
}
.budget-group + .budget-group {
margin-top: var(--sp-4);
}
.budget-group-head {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-2) 0;
margin-bottom: 4px;
border-bottom: 2px solid var(--border-strong);
}
.budget-cat {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: 7px 0;
border-bottom: 1px solid var(--border);
}
.budget-cat:last-child {
border-bottom: none;
}
.budget-cat-name {
display: flex;
align-items: center;
gap: var(--sp-2);
min-width: 0;
}
.budget-cat-bar {
flex: 1;
}
.amount-input {
width: 96px;
text-align: right;
margin-top: 0;
padding: 5px 8px;
font-variant-numeric: tabular-nums;
}
.budget-actual {
width: 96px;
text-align: right;
font-variant-numeric: tabular-nums;
}
.budget-slash {
margin: 0 -4px;
}
.budget-bar {
height: 7px;
background: var(--surface-2);
border-radius: 4px;
overflow: hidden;
}
.budget-bar-fill {
height: 100%;
background: var(--ok);
border-radius: 4px;
transition: width 0.2s;
}
.budget-bar-fill.over {
background: var(--danger);
}
.budget-total-row.card-total {
display: flex;
align-items: center;
gap: var(--sp-2);
padding-top: var(--sp-3);
margin-top: var(--sp-2);
border-top: 2px solid var(--border-strong);
}
tr.budget-total-row td {
border-top: 2px solid var(--border-strong);
padding-top: var(--sp-2);
}
/* Sparen-Option im Transfer-Formular. */
.savings-opt {
padding: var(--sp-3);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface-2);
margin-bottom: var(--sp-4);
}
.checkline {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-bottom: var(--sp-2);
}
.checkline span {
color: var(--text);
font-weight: 560;
}
/* ---------- Dashboard-Grid ---------- */
.dash-toolbar {
display: flex;
align-items: center;
gap: var(--sp-2);
position: relative;
}
.dash-add {
position: relative;
}
.dash-add-menu {
position: absolute;
right: 0;
top: calc(100% + 4px);
z-index: 20;
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
box-shadow: var(--shadow);
min-width: 200px;
overflow: hidden;
}
.dash-add-item {
display: block;
width: 100%;
text-align: left;
padding: 8px 12px;
border: none;
background: none;
color: var(--text);
cursor: pointer;
}
.dash-add-item:hover {
background: var(--surface-2);
}
/* Karten-Optionen im Anordnen-Modus (z. B. Gruppen vs. Kategorien). */
.dash-config-bar {
display: flex;
flex-wrap: wrap;
gap: var(--sp-3);
padding-bottom: var(--sp-2);
margin-bottom: var(--sp-2);
border-bottom: 1px solid var(--border);
}
.dash-config-opt {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 4px;
min-width: 0;
flex: 1 1 140px;
}
.seg.sm {
margin-bottom: 0;
width: 100%;
}
.seg.sm .seg-btn {
padding: 4px 9px;
font-size: 0.78rem;
}
.dash-grid {
position: relative;
}
.dash-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
overflow: hidden;
}
.dash-card-head {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 8px 12px;
border-bottom: 1px solid var(--border);
flex: 0 0 auto;
}
.dash-card-title {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
font-weight: 650;
flex: 1;
}
.dash-grid.editing .dash-card-head {
cursor: move;
background: var(--surface-2);
}
.dash-remove {
color: var(--danger);
}
.dash-card-body {
flex: 1;
min-height: 0;
overflow: auto;
padding: var(--sp-3);
}
.dash-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.dash-line {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 6px 0;
border-bottom: 1px solid var(--border);
}
.dash-line:last-child {
border-bottom: none;
}
.dash-total {
display: flex;
align-items: baseline;
gap: var(--sp-2);
padding-top: var(--sp-2);
margin-top: 2px;
border-top: 2px solid var(--border-strong);
font-variant-numeric: tabular-nums;
/* Die Summe bleibt stehen, waehrend die Liste darunter durchlaeuft. Sie ist
die Zahl, wegen der man auf die Karte schaut – sie darf nicht die eine
sein, die man wegscrollen muss. Klebt am unteren Rand des Rollbereichs,
also am Karten-Koerper; ein eigener Hintergrund, damit die Zeilen nicht
durchscheinen, und Abstand nach unten statt des Innenabstands der Karte. */
position: sticky;
bottom: calc(-1 * var(--sp-3));
background: var(--surface);
padding-bottom: var(--sp-3);
margin-bottom: calc(-1 * var(--sp-3));
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dash-loading, .dash-empty {
padding: var(--sp-4);
text-align: center;
}
.dash-progress {
padding: 6px 0;
}
.dash-progress-top {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-bottom: 4px;
}
.dash-kpis {
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--sp-2);
height: 100%;
}
.dash-kpi {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.dash-kpi strong {
font-size: 1.1rem;
font-variant-numeric: tabular-nums;
}
/* react-grid-layout: Platzhalter + Resize-Griff an das Theme angleichen. */
.react-grid-item.react-grid-placeholder {
background: var(--accent);
opacity: 0.15;
border-radius: var(--radius);
}
.react-grid-item > .react-resizable-handle::after {
border-right-color: var(--border-strong);
border-bottom-color: var(--border-strong);
}
/* ---------- CSV-Import ---------- */
.table-scroll {
overflow-x: auto;
}
.raw-table {
font-size: 0.8rem;
}
.raw-table th,
.raw-table td {
white-space: nowrap;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
}
.raw-table th {
color: var(--muted);
font-weight: 600;
}
.map-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: var(--sp-3);
margin-top: var(--sp-3);
}
.map-grid label {
margin-bottom: 0;
}
tr.row-invalid {
opacity: 0.45;
}
tr.row-dup {
opacity: 0.55;
}
.doc-text {
max-height: 240px;
overflow: auto;
font-size: 0.78rem;
white-space: pre-wrap;
color: var(--muted);
background: var(--surface-2);
padding: var(--sp-3);
border-radius: var(--radius-sm);
margin-top: var(--sp-2);
}
/* Beleg-Seiten als Bildstreifen (horizontal scrollbar). */
.doc-pages {
display: flex;
gap: var(--sp-3);
overflow-x: auto;
padding-bottom: var(--sp-2);
}
.doc-page {
height: 440px;
width: auto;
flex: 0 0 auto;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: #fff;
}
.import-more {
margin-top: var(--sp-3);
}
.import-more > summary {
cursor: pointer;
}
/* DataTable-Zellen einzeilig halten, damit „…" greift (Excel-artig). */
.datatable td { white-space: nowrap; vertical-align: middle; }
.datatable td .named-text { min-width: 0; }
/* Interaktive Zellen (Import-Prüfansicht) füllen ihre Spalte, statt auf 60% zu
schrumpfen (globale .table td select/input-Regel überschreiben). */
.datatable td select,
.datatable td input[inputmode],
.datatable td input[type="text"] { max-width: none; width: 100%; }
.datatable td .catsel { width: 100%; }
/* ---------- Kategorie-Auswahl (auf-/zuklappbarer Baum, Ersatz für <select>) ---------- */
.catsel {
width: 100%;
}
.catsel-btn {
width: 100%;
margin-top: 5px;
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 7px 10px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--text);
font: inherit;
text-align: left;
cursor: pointer;
transition: border-color .12s, box-shadow .12s;
}
.catsel-btn:hover:not(:disabled) {
border-color: var(--accent);
}
.catsel-btn:disabled {
opacity: 0.6;
cursor: default;
}
.catsel-btn.open {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.catsel-value {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: center;
gap: var(--sp-2);
}
.catsel-caret {
flex: 0 0 auto;
color: var(--muted);
}
.catsel-panel {
position: fixed;
z-index: 1000;
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--radius);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
overflow: hidden;
}
.catsel-search {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 8px 10px;
border-bottom: 1px solid var(--border);
color: var(--muted);
flex: 0 0 auto;
}
.catsel-search input {
width: 100%;
border: none;
background: none;
outline: none;
margin: 0;
padding: 0;
color: var(--text);
font: inherit;
}
.catsel-list {
overflow-y: auto;
padding: 5px;
}
.catsel-group + .catsel-group {
margin-top: 2px;
}
.catsel-group-head {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 6px;
border: none;
background: none;
cursor: pointer;
color: var(--muted);
font-weight: 650;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.catsel-group-head:hover {
color: var(--text);
}
.catsel-gchev {
flex: 0 0 auto;
transition: transform .12s ease;
}
.catsel-gchev.collapsed {
transform: rotate(-90deg);
}
.catsel-opt {
display: flex;
align-items: center;
gap: var(--sp-2);
width: 100%;
padding: 6px 8px 6px 22px;
border: none;
border-radius: var(--radius-sm);
background: none;
color: var(--text);
font: inherit;
text-align: left;
cursor: pointer;
}
.catsel-opt:hover {
background: var(--surface-2);
}
.catsel-opt.selected {
background: var(--accent-soft);
color: var(--accent);
font-weight: 600;
}
.catsel-check {
margin-left: auto;
flex: 0 0 auto;
color: var(--accent);
}
.catsel-empty {
padding: 10px;
}
/* ---------- Schlagwörter (Kategorie-Popup + Prüfmeldungen) ---------- */
.kw-add {
display: flex;
gap: var(--sp-2);
margin-bottom: var(--sp-3);
}
.kw-add input {
flex: 1 1 auto;
min-width: 0;
margin-top: 0;
}
.kw-chips {
display: flex;
flex-wrap: wrap;
gap: var(--sp-2);
}
.kw-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 6px 4px 11px;
border-radius: 20px;
background: var(--surface-2);
border: 1px solid var(--border);
font-size: 0.85rem;
}
.kw-amount {
color: var(--muted);
font-variant-numeric: tabular-nums;
font-size: 0.8em;
}
.kw-chip button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
padding: 0;
margin: 0;
border: none;
border-radius: 50%;
background: none;
color: var(--muted);
cursor: pointer;
}
.kw-chip button:hover {
background: var(--danger);
color: #fff;
}
/* Schlagwort-Knopf in der Kategorienliste. Belegte Kategorien tragen die
Akzentfarbe und die Anzahl – so sieht man der Liste an, wo schon etwas
hinterlegt ist, ohne jede Kategorie einzeln zu öffnen. */
.kw-btn {
gap: 5px;
}
.kw-btn.belegt {
color: var(--accent);
border-color: var(--accent-soft);
background: var(--accent-soft);
}
.kw-btn-zahl {
font-size: 0.72rem;
font-weight: 640;
font-variant-numeric: tabular-nums;
line-height: 1;
}
/* Das Schlagwort, das gerade oben in der Zeile liegt. Bearbeitet wird dort,
nicht im Chip – im Chip war für den Betrag kein Platz. */
.kw-chip.aktiv {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-soft);
}
/* Passend dazu die Eingabezeile hervorheben, damit klar ist, dass dort gerade
ein vorhandenes Wort liegt und nicht ein neues entsteht. */
.kw-add.bearbeitet input {
border-color: var(--accent);
}
.kw-flag {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3) 0;
border-bottom: 1px solid var(--border);
}
.kw-flag:last-child {
border-bottom: none;
}
.kw-flag-main {
flex: 1;
min-width: 0;
}
.kw-flag-word {
font-weight: 600;
}
/* ---------- Passwort: Anforderungsliste, Zwei-Faktor ---------- */
/* Nur für Screenreader – optisch weg, aber vorlesbar. */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* Hakt sich beim Tippen ab. Farben ausschliesslich aus Tokens, sonst bricht
das dunkle Thema. */
.pw-rules {
list-style: none;
margin: var(--sp-2) 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 3px;
font-size: 0.82rem;
}
.pw-rule {
display: flex;
align-items: center;
gap: var(--sp-2);
color: var(--muted);
transition: color 0.16s ease;
}
.pw-rule.erfuellt {
color: var(--ok);
}
.pw-rule-mark {
flex: 0 0 auto;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
/* Offener Punkt, solange die Regel nicht erfüllt ist. */
.pw-rule-dot {
width: 7px;
height: 7px;
border-radius: 50%;
border: 1.5px solid currentColor;
opacity: 0.55;
}
/* Kurzer Impuls, wenn das Häkchen erscheint. */
.pw-rule.erfuellt .pw-rule-mark {
animation: pw-rule-pop 0.18s ease-out;
}
@keyframes pw-rule-pop {
from { transform: scale(0.4); opacity: 0; }
60% { transform: scale(1.18); opacity: 1; }
to { transform: scale(1); opacity: 1; }
}
/* styles.css hat keinen globalen Bewegungs-Reset, nur eine Aufzählung –
deshalb hier noch einmal ausdrücklich für die neuen Bewegungen. */
@media (prefers-reduced-motion: reduce) {
.pw-rule { transition: none; }
.pw-rule.erfuellt .pw-rule-mark { animation: none; }
}
/* Zwei-Faktor: QR-Code, Secret und Wiederherstellungscodes. */
.tfa-qr {
display: block;
width: 190px;
height: 190px;
/* Der QR-Code braucht weissen Grund, damit er auch im dunklen Thema
zuverlässig gescannt wird. */
background: #fff;
padding: var(--sp-2);
border-radius: var(--radius-sm);
border: 1px solid var(--border);
}
.tfa-secret {
display: inline-block;
padding: 6px var(--sp-2);
border-radius: var(--radius-sm);
background: var(--surface-2);
border: 1px solid var(--border);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.9rem;
letter-spacing: 0.06em;
/* styles.css setzt body { user-select: none } – ohne das hier liesse sich
das Secret nicht markieren und kopieren. */
user-select: text;
}
.tfa-codes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: var(--sp-2);
margin: var(--sp-3) 0;
padding: var(--sp-3);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface-2);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.9rem;
user-select: text;
}
.tfa-codes .verbraucht {
color: var(--muted);
text-decoration: line-through;
}
/* Zusammenfassung der Prüfansicht beim Wiederherstellen. */
.restore-summary {
width: 100%;
border-collapse: collapse;
margin-top: var(--sp-2);
font-size: 0.88rem;
}
.restore-summary td {
padding: 3px 0;
border-bottom: 1px solid var(--border);
}
.restore-summary td:last-child {
text-align: right;
font-variant-numeric: tabular-nums;
color: var(--muted);
}
.restore-summary tr:last-child td {
border-bottom: none;
}
/* Zweiter Buchen-Knopf unter der Prüfliste – bei vielen Zeilen ist der obere
längst aus dem Bild gescrollt. */
.commit-footer {
display: flex;
justify-content: flex-end;
padding-top: var(--sp-3);
margin-top: var(--sp-2);
border-top: 1px solid var(--border);
}
/* Ausgaben-Anteile: der Prozentwert ist die Kernaussage der Karte und steht
deshalb vor dem Betrag, mit gleichbleibender Ziffernbreite. */
.share-pct {
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* Wird die Karte schmal, gibt zuerst der Betrag nach – ohne den Kategorienamen
sagt die Zeile nichts mehr aus, ohne den Betrag bleibt der Anteil lesbar. */
.share-row .share-name {
min-width: 4.5rem;
}
.share-row .share-amount {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Abgleich des selbst gerechneten Saldos mit dem, den die Bank fuehrt.
Eine Abweichung ist kein Fehler der Bank, sondern ein Hinweis auf eine
fehlende oder doppelte Buchung – deshalb warnend, nicht rot. */
.saldo-ok { color: var(--ok); }
.saldo-abweichung { color: var(--warn); font-weight: 560; }
/* ---------- Kreditrahmen ----------
Wie viel von der Karte noch uebrig ist. Der Balken faellt bewusst schmal
aus: Er begleitet den Saldo, er ersetzt ihn nicht. Neutral gefaerbt, denn
einen Rahmen auszunutzen ist nicht falsch – erst ihn zu ueberziehen. */
.kreditrahmen {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-top: 3px;
}
.rahmen-balken {
flex: 0 0 64px;
height: 5px;
background: var(--surface-2);
border-radius: 3px;
overflow: hidden;
}
.rahmen-fuellung {
height: 100%;
background: var(--muted);
border-radius: 3px;
transition: width 0.2s;
}
.rahmen-balken.ueber .rahmen-fuellung { background: var(--warn); }
/* ---------- Gebundenes Geld ----------
Steht unter „Gesamt", nicht darin. Aufklappbar, weil eine Summe, die man
nicht nachrechnen kann, man glauben muss. */
.gebunden-zeile {
padding: 0 var(--sp-4) var(--sp-2);
margin-top: -6px;
}
.gebunden-zeile summary {
cursor: pointer;
padding: 4px 0;
}
.gebunden-posten {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 3px 0 3px var(--sp-3);
font-size: 0.9em;
}
/* ---------- Übertrags-Vorschläge ---------- */
.uebertrag-vorschlag {
padding: var(--sp-3) 0;
border-bottom: 1px solid var(--border);
}
.uebertrag-vorschlag:last-child {
border-bottom: none;
}
/* Die beiden Seiten nebeneinander, damit man sie vergleicht statt sie zu
lesen. Auf schmalen Schirmen untereinander – gestapelt bleibt der Vergleich
möglich, gequetscht nicht. */
.uebertrag-seiten {
display: flex;
align-items: center;
gap: var(--sp-3);
}
.uebertrag-seite {
flex: 1 1 0;
min-width: 0;
}
.uebertrag-pfeil {
flex: 0 0 auto;
color: var(--muted);
}
@media (max-width: 820px) {
.uebertrag-seiten {
flex-direction: column;
align-items: stretch;
gap: var(--sp-2);
}
}
/* Übertrags-Marke in der Buchungsliste: gedämpft, nicht warnend – die Buchung
ist in Ordnung, sie zählt nur nicht als Ausgabe oder Einnahme. */
.uebertrag-marke {
display: flex;
align-items: center;
gap: 4px;
color: var(--accent);
}
/* ---------- Einstellungen: Abschnittsleiste + gestapelte Abschnitte ----------
Vorher lagen die Karten in einem Spaltenfluss nebeneinander. Man fand nichts
wieder, weil die Reihenfolge von der Kartenhöhe abhing statt vom Thema.
Jetzt: eine Spalte in fester Reihenfolge, daneben eine Leiste, die zeigt, wo
man ist, und mit einem Klick dorthin springt. */
.settings-layout {
display: flex;
align-items: flex-start;
gap: var(--sp-5);
}
.settings-nav {
flex: 0 0 200px;
position: sticky;
top: var(--sp-5);
/* Eigener Bildlauf, falls die Liste einmal höher wird als der Schirm –
sonst wären die untersten Punkte nicht erreichbar. */
max-height: calc(100vh - var(--sp-6) * 2);
overflow-y: auto;
}
.settings-nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.settings-nav a {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 8px 10px;
border-radius: var(--radius-sm);
color: var(--muted);
text-decoration: none;
font-weight: 560;
border-left: 2px solid transparent;
min-width: 0;
}
.settings-nav a:hover {
background: var(--surface-2);
color: var(--text);
}
.settings-nav a.active {
background: var(--surface-2);
color: var(--accent);
border-left-color: var(--accent);
}
.settings-inhalt {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--sp-4);
}
/* Beim Anspringen soll die Überschrift nicht am oberen Rand kleben. */
.settings-section {
scroll-margin-top: var(--sp-5);
}
@media (max-width: 1000px) {
/* Drei Spalten passen nicht mehr: Die Abschnittsleiste klappt über den
Inhalt und wird zur waagerecht scrollbaren Reihe. */
.settings-layout {
flex-direction: column;
/* Ohne das erbt die Spalten-Anordnung das flex-start von oben – dort ist
es für die klebende Leiste nötig, hier presst es Leiste und Inhalt auf
ihre Inhaltsbreite zusammen statt auf die volle. */
align-items: stretch;
}
.settings-nav {
position: sticky;
top: 0;
z-index: 5;
flex: 0 0 auto;
width: 100%;
max-height: none;
padding: var(--sp-2) 0;
background: var(--bg);
border-bottom: 1px solid var(--border);
}
.settings-nav ul {
flex-direction: row;
overflow-x: auto;
gap: var(--sp-2);
}
.settings-nav a {
white-space: nowrap;
border-left: none;
border-bottom: 2px solid transparent;
}
.settings-nav a.active {
border-left-color: transparent;
border-bottom-color: var(--accent);
}
.settings-section {
scroll-margin-top: 56px;
}
}
@media (max-width: 760px) {
/* Ab hier ist die Hauptnavigation selbst eine klebende Leiste am oberen Rand
(z-index 20). Eine zweite klebende Leiste darunter verschwände hinter ihr –
also scrollt die Abschnittsleiste hier einfach mit. */
.settings-nav {
position: static;
border-bottom: none;
}
.settings-section {
scroll-margin-top: var(--sp-3);
}
}
/* Kategorie-Auswahl in einer Tabellenzeile: eigene Spalte, also volle
Zellenbreite – aber ohne den Abstand, den sie im Formular unter ihrer
Beschriftung braucht. Der verschöbe sie gegen die übrigen Zellen. */
.datatable td .catsel-btn {
margin-top: 0;
padding: 4px 8px;
}
/* Herkunft einer Buchung: Bank, Mail, Kurzbefehl, Datei, von Hand. Sie steht
NEBEN dem Titel und in dessen Grösse – nicht kleingedruckt darunter, wo sich
sonst alles Neue stapelt. */
.titelzeile {
display: flex;
align-items: center;
gap: var(--sp-2);
min-width: 0;
}
.titelzeile > .ellipsis {
min-width: 0;
}
/* Aussehen kommt von `.badge` – dieselbe Pille wie überall sonst. Hier steht
nur, wie sie sich in der Titelzeile verhält: nicht schrumpfen, nicht
umbrechen. Eng wird zuerst der Titel, denn ein halbes „Datei-Imp…" nützt
niemandem, ein gekürzter Titel bleibt lesbar. */
.herkunft {
flex: 0 0 auto;
white-space: nowrap;
}
/* ---------- Bankanbindung: Verbindung, Konto, neue Bank ----------
Vorher lieh sich das die Klassen des Postfachs: gestrichelte Trennlinien,
sonst nichts. Damit war nicht zu sehen, welcher Knopf zu welchem Konto
gehört und wo eine Bank aufhört. */
.bank-verbindung {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--sp-4);
margin-top: var(--sp-4);
}
.bank-verbindung-kopf {
display: flex;
align-items: center;
gap: var(--sp-2);
padding-bottom: var(--sp-2);
margin-bottom: var(--sp-2);
border-bottom: 1px solid var(--border);
}
/* Ein Konto der Verbindung: eingerückt und mit eigener Kante. Die Kante ist
das, was seine Knöpfe sichtbar an es bindet. */
.bank-konto {
border-left: 2px solid var(--border-strong);
padding: var(--sp-3) 0 var(--sp-3) var(--sp-3);
margin-top: var(--sp-3);
}
.bank-konto + .bank-konto {
margin-top: var(--sp-2);
}
.bank-konto select {
width: 100%;
margin-top: 4px;
}
/* Die Knöpfe der VERBINDUNG (alle Konten, Salden, trennen) stehen ohne
Einrückung – so gehören sie sichtbar zur Bank und nicht zum letzten Konto. */
.bank-verbindung > .mail-actions {
padding-top: var(--sp-3);
border-top: 1px solid var(--border);
}
/* Eine neue Bank hinzufügen ist etwas anderes als eine bestehende zu pflegen. */
.bank-neu {
margin-top: var(--sp-5);
padding-top: var(--sp-4);
border-top: 2px solid var(--border-strong);
}
.bank-neu h3 {
margin: 0 0 var(--sp-3);
font-size: 1rem;
}
/* Die Einheit neben dem Zahlenfeld darf nicht umbrechen – „Minuten (0" in der
einen und „= aus)" in der nächsten Zeile las sich wie ein Fehler. */
.bank-verbindung .input-with-button > .muted {
white-space: nowrap;
}
/* Rückfrage in der Buchungszeile: „Das könnte ein Übertrag sein." Bewusst
auffällig genug, um gesehen zu werden – sie verlangt eine Antwort, anders
als die Marke einer schon bestätigten Verknüpfung. */
.uebertrag-frage {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-top: 3px;
font-size: 0.8rem;
color: var(--warn);
min-width: 0;
}
.uebertrag-frage .btn {
flex: 0 0 auto;
}
/* Aufgeschlüsselter Kontostand: eine Rechnung, also untereinander und rechts
ausgerichtet – so lässt sie sich nachvollziehen wie auf Papier. */
/* Die Zeile darf nur umbrechen, wenn die Rechnung offen ist – sonst
verschiebt sich die Kontenliste ohne Grund. */
.account-row.mit-aufschluss {
flex-wrap: wrap;
}
.aufschluss {
flex: 1 1 100%;
margin: var(--sp-2) 0 0 38px;
padding: var(--sp-3);
border-left: 2px solid var(--border-strong);
font-size: 0.85rem;
}
.aufschluss-zeile {
display: flex;
justify-content: space-between;
gap: var(--sp-3);
padding: 2px 0;
min-width: 0;
}
.aufschluss-zeile > span:last-child {
font-variant-numeric: tabular-nums;
}
.aufschluss-summe {
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid var(--border-strong);
font-weight: 600;
}
.aufschluss-uebertraege {
margin-top: var(--sp-3);
padding-top: var(--sp-2);
border-top: 1px dashed var(--border);
}
/* Budget-Stand im Ringdiagramm: grün im Rahmen, rot darüber – in der Legende
und in der Mitte. Ohne Plan bleibt beides, wie es war: Da gibt es nichts zu
überschreiten, und eine Farbe behauptete etwas. */
.legend-value.budget-ok,
.donut-value.budget-ok {
fill: var(--ok);
color: var(--ok);
}
.legend-value.budget-over,
.donut-value.budget-over {
fill: var(--danger);
color: var(--danger);
}
/* Gleich aussehende Buchungen: die Gruppe als Block, die Zeilen darin
eingerückt – so sieht man auf einen Blick, wie viele es sind. */
.dubletten-gruppe {
padding: var(--sp-3) 0;
border-bottom: 1px solid var(--border);
}
.dubletten-gruppe:last-child {
border-bottom: none;
}
.dubletten-zeile {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 3px 0 3px var(--sp-3);
border-left: 2px solid var(--border-strong);
margin-top: 4px;
min-width: 0;
}
.dubletten-zeile > .ellipsis {
flex: 1 1 auto;
min-width: 0;
}
/* Zeitraum einer Dashboard-Karte: leise, aber da – ohne ihn ist bei einem
Budget-Monat, der nicht am Ersten beginnt, nicht nachvollziehbar, was
gezählt wurde. */
.dash-zeitraum {
margin-bottom: var(--sp-2);
}
/* ---------- Budgetzeitraum einer Buchung ----------
Normalerweise entscheidet das Datum, und dann steht hier nur, was gilt. Das
Ändern liegt hinter einem Klick und hinter Pfeilen statt hinter einer
Auswahlliste: Es ist der Ausnahmefall und soll sich auch so anfühlen. */
.budgetzeitraum {
margin-bottom: var(--sp-4);
}
.budgetzeitraum-kopf {
display: flex;
align-items: center;
gap: var(--sp-2);
min-height: 32px;
}
.budgetzeitraum-wahl {
margin-top: var(--sp-2);
padding: var(--sp-3);
border: 1px dashed var(--border-strong);
border-radius: var(--radius-sm);
}
.budgetzeitraum-wahl p {
margin: 0 0 var(--sp-3);
}
.budgetzeitraum-schritte {
display: flex;
align-items: center;
gap: var(--sp-2);
flex-wrap: wrap;
}
/* Feste Breite: Sonst springen die Pfeile, sobald „Mai" auf „September" folgt. */
.budgetzeitraum-monat {
min-width: 9.5em;
text-align: center;
font-weight: 580;
color: var(--text);
}
/* ---------- Woraus ein Anteil besteht ----------
Eine Zahl, die man nicht aufschlüsseln kann, muss man glauben. Deshalb ist
jede Zeile der Ausgaben-Anteile ein Knopf – sie sieht aus wie vorher, führt
aber zu den Buchungen dahinter. */
.anteil-knopf {
display: block;
width: 100%;
background: none;
border: 0;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
text-align: left;
cursor: pointer;
border-radius: var(--radius-sm);
}
.anteil-knopf:hover .share-name {
text-decoration: underline;
}
.chart-legend li > .legend-zeile {
display: flex;
align-items: center;
gap: var(--sp-2);
width: 100%;
min-width: 0;
background: none;
border: 0;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
text-align: left;
}
.chart-legend li > .legend-zeile.klickbar {
cursor: pointer;
}
.chart-legend li > .legend-zeile.klickbar:hover .legend-name {
text-decoration: underline;
}
.anteil-liste {
display: flex;
flex-direction: column;
}
.anteil-zeile {
display: flex;
align-items: baseline;
gap: var(--sp-3);
padding: 6px 0;
border-bottom: 1px solid var(--border);
font-size: 0.85rem;
}
/* Feste Breite für Datum und Konto: Sonst rutscht der Betrag von Zeile zu
Zeile, und genau die Beträge will man hier untereinander vergleichen. */
.anteil-datum {
flex: 0 0 auto;
min-width: 5.5em;
}
.anteil-konto {
flex: 0 1 auto;
max-width: 9em;
}
/* ---------- Halbes Update ----------
Gebaut, aber der Dienst nicht neu gestartet: Die Oberfläche ruft dann
Endpunkte auf, die es serverseitig noch gar nicht gibt. Ohne diesen Hinweis
sieht das aus wie ein Fehler in der Funktion. */
.stand-hinweis {
display: flex;
align-items: flex-start;
gap: var(--sp-2);
margin-bottom: var(--sp-4);
padding: var(--sp-3) var(--sp-4);
border: 1px solid var(--warn);
border-radius: var(--radius-sm);
background: var(--warn-soft);
color: var(--text);
font-size: 0.85rem;
line-height: 1.45;
}
.stand-hinweis svg {
flex: 0 0 auto;
margin-top: 2px;
}
.stand-hinweis code {
padding: 1px 5px;
border-radius: 4px;
background: var(--surface-2);
font-size: 0.95em;
}
/* --- Abos & Verträge ----------------------------------------------------- */
/* Die Summe ganz oben: Sie ist die Antwort auf die Frage, wegen der man die
Seite überhaupt öffnet. */
.abo-summe {
display: flex;
flex-wrap: wrap;
gap: var(--sp-5);
}
.abo-summe-zahl {
font-size: 1.4rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
margin-top: 2px;
}
.abo-liste {
display: flex;
flex-direction: column;
}
/* Als Gitter und nicht als Reihe: Auf dem Telefon rutscht der Betrag unter den
Namen, statt vier Spalten in 375 Pixel zu quetschen. Mit Reihe blieb vom
Namen "Sal..." uebrig und von der Kategorie ein Punkt. */
.abo-zeile {
display: grid;
grid-template-columns: auto 1fr auto auto;
grid-template-areas: "symbol mitte betrag knoepfe";
align-items: center;
gap: var(--sp-3);
padding: var(--sp-2) 0;
border-bottom: 1px solid var(--border);
}
.abo-zeile > :nth-child(1) { grid-area: symbol; }
.abo-mitte { grid-area: mitte; }
.abo-betrag { grid-area: betrag; }
.abo-knoepfe { grid-area: knoepfe; }
.abo-zeile:last-child { border-bottom: none; }
/* Gekündigtes bleibt lesbar, tritt aber zurück – es zählt nicht mehr mit. */
.abo-beendet { opacity: 0.55; }
.abo-mitte { min-width: 0; }
.abo-kopf {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 2px var(--sp-2);
min-width: 0;
}
.abo-unten {
display: flex;
flex-wrap: wrap;
gap: 0 5px;
min-width: 0;
margin-top: 1px;
}
.abo-faellig { color: var(--muted); white-space: nowrap; }
.abo-faellig.warn { color: var(--warn); }
.abo-betrag {
display: flex;
flex-direction: column;
align-items: flex-end;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.abo-knoepfe { display: flex; gap: 2px; }
/* Das Ansparen gehört sichtbar zum Intervall darüber, nicht daneben. */
.abo-ansparen {
border-left: 2px solid var(--border-strong);
padding-left: var(--sp-3);
margin-bottom: var(--sp-3);
}
@media (max-width: 620px) {
/* Betrag unter den Namen, Knoepfe bleiben rechts oben. So bekommt der Name
die volle Breite – auf dem Telefon ist er das Einzige, woran man die Zeile
erkennt. */
.abo-zeile {
grid-template-columns: auto 1fr auto;
grid-template-areas:
"symbol mitte knoepfe"
"symbol betrag knoepfe";
align-items: start;
row-gap: 2px;
padding: var(--sp-3) 0;
}
.abo-zeile > :nth-child(1),
.abo-knoepfe { align-self: center; }
/* Waagrecht statt untereinander: Unter dem Namen ist Breite da, Hoehe nicht. */
.abo-betrag {
flex-direction: row;
align-items: baseline;
flex-wrap: wrap;
gap: 0 var(--sp-2);
}
.abo-summe { gap: var(--sp-3) var(--sp-5); }
.abo-summe-zahl { font-size: 1.2rem; }
}
/* Waehrungs-Auswahl in den Einstellungen: ein Gitter aus Kacheln statt einer
Liste mit dreissig Zeilen. */
.waehrung-gitter {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
gap: 6px;
margin-bottom: var(--sp-2);
}
.waehrung-kachel {
display: flex;
align-items: center;
gap: 6px;
margin: 0;
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: var(--radius-sm, 6px);
cursor: pointer;
font-size: 0.85rem;
white-space: nowrap;
}
.waehrung-kachel.an { border-color: var(--accent, var(--border-strong)); }
.waehrung-kachel input { width: auto; margin: 0; }
.waehrung-kachel:has(input:disabled) { opacity: 0.6; cursor: default; }
/* Logo statt Symbol. Gleiche Kantenlänge wie der IconChip daneben, damit die
Liste nicht wackelt, je nachdem ob ein Vertrag ein Logo hat. */
.abo-logo {
width: 30px;
height: 30px;
object-fit: contain;
border-radius: 6px;
flex: none;
}
.abo-logo.gross { width: 48px; height: 48px; border-radius: 8px; }
.abo-logo-feld {
display: flex;
align-items: center;
gap: var(--sp-3);
margin-bottom: var(--sp-2);
}
.abo-logo-knoepfe { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
/* Das Dateifeld steckt in einem <label>, das wie ein Knopf aussieht. */
.abo-logo-knoepfe label.btn { cursor: pointer; margin: 0; }
.abo-logo-knoepfe label.btn.deaktiviert { opacity: 0.5; cursor: not-allowed; }
/* --- Logo zuschneiden ---------------------------------------------------- */
.zuschnitt-flaeche {
position: relative;
margin: 0 auto var(--sp-2);
border-radius: var(--radius);
/* Schachbrett: zeigt, wo das Bild durchsichtig ist. Bei einem freigestellten
Logo ist das der Unterschied zwischen "weisser Kasten" und "nichts da". */
background:
repeating-conic-gradient(var(--border) 0% 25%, transparent 0% 50%) 50% / 16px 16px;
touch-action: none;
user-select: none;
overflow: hidden;
}
.zuschnitt-flaeche img {
display: block;
width: 100%;
height: 100%;
pointer-events: none;
}
.zuschnitt-rahmen {
position: absolute;
box-sizing: border-box;
border: 1px solid #fff;
cursor: move;
/* Alles ausserhalb abdunkeln – mit EINEM Schatten statt vier Kacheln.
So bleibt der Ausschnitt das Helle, und man sieht sofort, was bleibt. */
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
}
/* Drittel-Linien: helfen beim Ausrichten, ohne den Ausschnitt zuzustellen. */
.zuschnitt-rahmen::before,
.zuschnitt-rahmen::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.35;
}
.zuschnitt-rahmen::before {
border-left: 1px solid #fff;
border-right: 1px solid #fff;
left: 33.33%;
right: 33.33%;
}
.zuschnitt-rahmen::after {
border-top: 1px solid #fff;
border-bottom: 1px solid #fff;
top: 33.33%;
bottom: 33.33%;
}
.zuschnitt-griff {
position: absolute;
width: 14px;
height: 14px;
background: #fff;
border-radius: 3px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
}
.griff-nw { left: -7px; top: -7px; cursor: nwse-resize; }
.griff-n { left: calc(50% - 7px); top: -7px; cursor: ns-resize; }
.griff-ne { right: -7px; top: -7px; cursor: nesw-resize; }
.griff-w { left: -7px; top: calc(50% - 7px); cursor: ew-resize; }
.griff-e { right: -7px; top: calc(50% - 7px); cursor: ew-resize; }
.griff-sw { left: -7px; bottom: -7px; cursor: nesw-resize; }
.griff-s { left: calc(50% - 7px); bottom: -7px; cursor: ns-resize; }
.griff-se { right: -7px; bottom: -7px; cursor: nwse-resize; }
.zuschnitt-fuss {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-bottom: var(--sp-2);
}
.zuschnitt-fuss span { flex: 1; font-variant-numeric: tabular-nums; }
/* Auf dem Telefon sind die Griffe mit dem Finger sonst nicht zu treffen. */
@media (pointer: coarse) {
.zuschnitt-griff { width: 20px; height: 20px; }
.griff-nw, .griff-w, .griff-sw { left: -10px; }
.griff-ne, .griff-e, .griff-se { right: -10px; }
.griff-nw, .griff-n, .griff-ne { top: -10px; }
.griff-sw, .griff-s, .griff-se { bottom: -10px; }
.griff-n, .griff-s { left: calc(50% - 10px); }
.griff-w, .griff-e { top: calc(50% - 10px); }
}
/* --- Budget aus Verträgen ------------------------------------------------ */
/* Die Rechnung unter der Zeile. Eingerückt bis unter den Namen, damit sie zur
Zeile darüber gehört und nicht wie eine eigene Kategorie aussieht. */
.budget-rechnung {
padding: 0 0 6px 30px;
margin-top: -4px;
font-variant-numeric: tabular-nums;
}
.budget-hinweis { margin: 0 0 var(--sp-2); }
/* Der Umschalter. Schmal – er steht zwischen Ist-Betrag und Eingabefeld, und
dort ist wenig Platz. */
.btn-sigma {
flex: none;
width: 26px;
height: 26px;
padding: 0;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--muted);
font-size: 0.9rem;
line-height: 1;
cursor: pointer;
}
.btn-sigma:hover { border-color: var(--border-strong); color: var(--fg); }
.btn-sigma.an {
border-color: var(--accent, var(--border-strong));
color: var(--accent, var(--fg));
background: var(--accent-soft, transparent);
}
/* Die Aufschlüsselung unter einer Budget-Zeile. Aufklappbar, weil eine Summe,
die man nicht nachrechnen kann, geglaubt werden muss. */
.budget-rechnung-kopf {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
padding: 0;
border: 0;
background: none;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.budget-rechnung-kopf .icon { color: var(--muted); flex: none; }
.budget-rechnung-kopf:hover .muted { color: var(--fg); }
.budget-rechnung-kopf .warn { margin-left: auto; white-space: nowrap; }
.budget-vertragsliste {
list-style: none;
margin: 2px 0 6px;
padding: 0 0 0 17px;
}
.budget-vertragsliste li {
display: flex;
gap: var(--sp-2);
padding: 1px 0;
}
.budget-vertragsliste li > :first-child { flex: 1; min-width: 0; }
.budget-vertrag-betrag { white-space: nowrap; font-variant-numeric: tabular-nums; }
/* --- Anmeldung über einen fremden Dienst --------------------------------- */
/* Trennlinie mit „oder" auf der Login-Seite: Der SSO-Knopf ist eine
Alternative zum Formular darüber, keine Fortsetzung davon. */
.login-trenner {
display: flex;
align-items: center;
gap: var(--sp-3);
margin: var(--sp-4) 0 var(--sp-3);
color: var(--muted);
font-size: 0.85rem;
}
.login-trenner::before,
.login-trenner::after {
content: "";
flex: 1;
height: 1px;
background: var(--border);
}
.btn.login-sso {
width: 100%;
justify-content: center;
text-decoration: none;
}
.sso-liste { display: flex; flex-direction: column; gap: var(--sp-2); }
.sso-zeile {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 6px 0;
border-bottom: 1px solid var(--border);
}
.sso-zeile:last-child { border-bottom: none; }
.sso-haken { color: var(--ok); flex: none; }
.btn.sso-verknuepfen { justify-content: center; text-decoration: none; }