Finanzen: Einstellungen, Bankzugang, Fixkosten und Bankrueckkehr ohne Karten

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 14:38:00 +00:00
1 parent b5d27d5801
commit d1a1046969
7 files changed
+91 -126

No files matched your search

+16 -27
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react'
import { FaRotate, FaSpinner } from 'react-icons/fa6'
import { Fragment, useEffect, useRef, useState } from 'react'
import { Badge, Button, Card } from '../ui'
import { useFinanceResource } from '../../hooks/useFinanceResource'
import { noticeProps } from '../../lib/financeUi'
import { getBank, refreshBank, startBankAuth } from '../../lib/financeApi'
import { bankCardModel, refreshResultMessage } from '../../lib/financeSettings'
import { safeUrl } from '../../lib/financeView'
@@ -67,14 +67,11 @@ export default function BankAccessCard() {
return (
<Card title="Bankzugang">
{res.loading ? (
<div className="empty" role="status">
<FaSpinner className="spinner" aria-hidden="true" />
<p className="muted">Bankzugang wird geladen …</p>
</div>
<div className="loading" role="status">Bankzugang wird geladen …</div>
) : (
<div className="fin-load-error" style={{ padding: 0 }}>
<p className="fin-hint fin-hint-warn" role="alert">{res.error || LOAD_ERROR}</p>
<Button size="sm" onClick={res.reload}>Erneut versuchen</Button>
<div className="fin-load-error">
<p {...noticeProps('warn')}>{res.error || LOAD_ERROR}</p>
<Button onClick={res.reload}>Erneut versuchen</Button>
</div>
)}
</Card>
@@ -85,36 +82,28 @@ export default function BankAccessCard() {
return (
<Card title="Bankzugang" actions={<Badge tone={model.tone}>{model.statusLabel}</Badge>}>
{res.error && (
<p className="fin-hint fin-hint-warn fin-form-notice" role="alert">
Der Stand konnte nicht neu geladen werden: {res.error}
</p>
<p {...noticeProps('warn', 'fin-notice')}>Der Stand konnte nicht neu geladen werden: {res.error}</p>
)}
{model.rows.length > 0 && (
<dl className="fin-bank-facts">
<dl className="kv fin-kv">
{model.rows.map((row) => (
<div key={row.key}>
<Fragment key={row.key}>
<dt>{row.label}</dt>
<dd>{row.text}</dd>
</div>
<dd className="num">{row.text}</dd>
</Fragment>
))}
</dl>
)}
{model.error && (
<p className="fin-hint fin-hint-warn fin-form-notice" role="alert">Fehler: {model.error}</p>
)}
{model.hint && <p className="fin-hint fin-form-notice">{model.hint}</p>}
{message && (
<p className={`fin-hint fin-form-notice${message.tone === 'warn' ? ' fin-hint-warn' : ''}`} role={message.tone === 'warn' ? 'alert' : 'status'}>
{message.text}
</p>
)}
{model.error && <p {...noticeProps('warn', 'fin-notice')}>Fehler: {model.error}</p>}
{model.hint && <p {...noticeProps('info', 'fin-notice')}>{model.hint}</p>}
{message && <p {...noticeProps(message.tone, 'fin-notice')}>{message.text}</p>}
<div className="fin-form-actions">
{model.canRefresh && (
<Button variant="primary" onClick={refresh} disabled={busy !== null}>
<FaRotate className={busy === 'refresh' ? 'fin-spin' : undefined} aria-hidden="true" /> {busy === 'refresh' ? 'Rufe ab …' : 'Jetzt abrufen'}
{busy === 'refresh' ? 'Rufe ab …' : 'Jetzt abrufen'}
</Button>
)}
<Button variant={model.canRefresh ? 'ghost' : 'primary'} onClick={connect} disabled={busy !== null}>
<Button variant={model.canRefresh ? 'default' : 'primary'} onClick={connect} disabled={busy !== null}>
{busy === 'connect' ? 'Leite weiter …' : model.connectLabel}
</Button>
</div>
+25 -30
View File
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { FaPlus, FaSpinner } from 'react-icons/fa6'
import { Badge, Button, Card, EmptyState } from '../ui'
import FormField from './FormField'
import { useFinanceResource } from '../../hooks/useFinanceResource'
import { noticeProps } from '../../lib/financeUi'
import { createFixedCost, deleteFixedCost, getFixedCosts, updateFixedCost } from '../../lib/financeApi'
import {
FIXED_COST_DELETE_CONFIRM,
@@ -146,45 +146,44 @@ export default function FixedCostsCard() {
let body
if (!res.data) {
body = res.loading ? (
<div className="empty" role="status">
<FaSpinner className="spinner" aria-hidden="true" />
<p className="muted">Fixkosten werden geladen …</p>
</div>
<div className="loading" role="status">Fixkosten werden geladen …</div>
) : (
<div className="fin-load-error" style={{ padding: 0 }}>
<p className="fin-hint fin-hint-warn" role="alert">{res.error || LOAD_ERROR}</p>
<Button size="sm" onClick={res.reload}>Erneut versuchen</Button>
<div className="fin-load-error">
<p {...noticeProps('warn')}>{res.error || LOAD_ERROR}</p>
<Button onClick={res.reload}>Erneut versuchen</Button>
</div>
)
} else {
body = (
<>
{res.error && (
<p className="fin-hint fin-hint-warn" role="alert">
<p {...noticeProps('warn', 'fin-notice')}>
Die Liste konnte nicht neu geladen werden: {res.error}{' '}
<Button size="sm" variant="ghost" onClick={res.reload} disabled={res.loading}>Erneut versuchen</Button>
<button type="button" className="link" onClick={res.reload} disabled={res.loading}>Erneut versuchen</button>
</p>
)}
{editing && (
<form className="fin-fixed-form" onSubmit={submit} noValidate aria-labelledby="fin-fix-form-title">
<h4 id="fin-fix-form-title" className="fin-fixed-form-title">{editing.id ? 'Position bearbeiten' : 'Neue Position'}</h4>
<h3 id="fin-fix-form-title" className="fin-form-title">{editing.id ? 'Position bearbeiten' : 'Neue Position'}</h3>
<fieldset className="fin-fieldset" disabled={busy === 'save'}>
<div className="fin-form-grid">
<div className="field-row">
<FormField id={fieldId('name')} label="Name" error={errors.name}>
<input className="form-control" autoComplete="off" value={form.name} onChange={change('name')} placeholder="z. B. Hetzner" />
<input className="input" autoComplete="off" value={form.name} onChange={change('name')} placeholder="z. B. Hetzner" />
</FormField>
<FormField id={fieldId('amount')} label="Betrag (€)" error={errors.amount}>
<input className="form-control fin-input-amount" inputMode="decimal" autoComplete="off" value={form.amount} onChange={change('amount')} placeholder="z. B. 49,90" />
<input className="input fin-input-amount" inputMode="decimal" autoComplete="off" value={form.amount} onChange={change('amount')} placeholder="z. B. 49,90" />
</FormField>
</div>
<div className="field-row">
<FormField id={fieldId('interval')} label="Rhythmus" error={errors.interval}>
<select className="form-control" value={form.interval} onChange={change('interval')}>
<select className="select" value={form.interval} onChange={change('interval')}>
{INTERVAL_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</FormField>
<FormField id={fieldId('nextDate')} label="Nächster Termin" error={errors.nextDate}>
<input className="form-control" type="date" value={form.nextDate} onChange={change('nextDate')} />
<input className="input" type="date" value={form.nextDate} onChange={change('nextDate')} />
</FormField>
</div>
<FormField
@@ -193,22 +192,18 @@ export default function FixedCostsCard() {
hint={`Teil des Namens der Gegenpartei in der Bankbuchung, z. B. hetzner (höchstens ${SETTINGS_LIMITS.maxMatch} Zeichen). Damit schlägt die Seite diese Abbuchung nicht noch einmal als neue Fixkosten vor.`}
error={errors.counterpartyMatch}
>
<input className="form-control" autoComplete="off" value={form.counterpartyMatch} onChange={change('counterpartyMatch')} />
<input className="input" autoComplete="off" value={form.counterpartyMatch} onChange={change('counterpartyMatch')} />
</FormField>
</fieldset>
<div className="fin-form-actions">
<Button type="submit" variant="primary" disabled={busy !== null}>
{busy === 'save' ? 'Speichere …' : 'Speichern'}
</Button>
<Button type="button" variant="ghost" onClick={closeForm} disabled={busy !== null}>Abbrechen</Button>
<Button type="button" onClick={closeForm} disabled={busy !== null}>Abbrechen</Button>
</div>
</form>
)}
{message && (
<p className={`fin-hint fin-form-notice${message.tone === 'warn' ? ' fin-hint-warn' : ''}`} role={message.tone === 'warn' ? 'alert' : 'status'}>
{message.text}
</p>
)}
{message && <p {...noticeProps(message.tone, 'fin-notice')}>{message.text}</p>}
{rows.length === 0 ? (
<EmptyState title="Noch keine Fixkosten." hint="Lege regelmäßige Ausgaben an, damit Vorschau und „Verfügbar“ sie berücksichtigen." />
) : (
@@ -222,13 +217,13 @@ export default function FixedCostsCard() {
<div className="fin-amount fin-fixed-amount">{row.amountText}</div>
<div className="fin-fixed-meta muted">{row.metaText}</div>
<div className="fin-actions fin-fixed-actions">
<Button size="sm" variant="ghost" onClick={() => openEdit(row)} disabled={busy !== null} aria-label={`Bearbeiten: ${row.name}`}>
<button type="button" className="link" onClick={() => openEdit(row)} disabled={busy !== null} aria-label={`Bearbeiten: ${row.name}`}>
Bearbeiten
</Button>
<Button size="sm" variant="ghost" onClick={() => toggle(row)} disabled={busy !== null} aria-label={`${row.toggleLabel}: ${row.name}`}>
</button>
<button type="button" className="link" onClick={() => toggle(row)} disabled={busy !== null} aria-label={`${row.toggleLabel}: ${row.name}`}>
{busy === `${row.id}:toggle` ? 'Speichere …' : row.toggleLabel}
</Button>
<Button size="sm" variant="danger" onClick={() => remove(row)} disabled={busy !== null} aria-label={`Löschen: ${row.name}`}>
</button>
<Button type="button" variant="danger" onClick={() => remove(row)} disabled={busy !== null} aria-label={`Löschen: ${row.name}`}>
{busy === `${row.id}:delete` ? 'Lösche …' : 'Löschen'}
</Button>
</div>
@@ -245,8 +240,8 @@ export default function FixedCostsCard() {
title="Fixkosten"
actions={
res.data && (
<Button size="sm" variant="primary" onClick={openNew} disabled={busy !== null || editing?.id === null}>
<FaPlus aria-hidden="true" /> Neue Position
<Button onClick={openNew} disabled={busy !== null || editing?.id === null}>
Neue Position
</Button>
)
}
+5 -4
View File
@@ -1,8 +1,9 @@
import { cloneElement } from 'react'
/**
* Formularfeld mit Beschriftung, Hinweis und Fehlermeldung. `children` ist genau ein Eingabeelement; es bekommt
* `id`, `aria-invalid` und `aria-describedby` (Hinweis und Fehler). Die Fehlermeldung wird vorgelesen (role="alert").
* Formularfeld mit Beschriftung, Hinweis und Fehlermeldung (Klassen des Bausteins `Field`: `.field`, `.hint`, `.msg.err`).
* `children` ist genau ein Eingabeelement; es bekommt `id`, `aria-invalid` und `aria-describedby` (Hinweis und Fehler).
* Den roten Ring am Feld liefert allein `aria-invalid`. Die Fehlermeldung wird vorgelesen (role="alert").
*/
export default function FormField({ id, label, hint, error, children }) {
const hintId = hint ? `${id}-hint` : undefined
@@ -13,12 +14,12 @@ export default function FormField({ id, label, hint, error, children }) {
<label htmlFor={id}>{label}</label>
{cloneElement(children, { id, 'aria-invalid': error ? 'true' : undefined, 'aria-describedby': describedBy })}
{hint && (
<div id={hintId} className="faint fin-form-hint">
<div id={hintId} className="hint">
{hint}
</div>
)}
{error && (
<div id={errorId} className="fin-field-error" role="alert">
<div id={errorId} className="msg err" role="alert">
{error}
</div>
)}
+15 -20
View File
@@ -1,8 +1,8 @@
import { useEffect, useRef, useState } from 'react'
import { FaFloppyDisk, FaSpinner } from 'react-icons/fa6'
import { Button, Card } from '../ui'
import FormField from './FormField'
import { useFinanceResource } from '../../hooks/useFinanceResource'
import { noticeProps } from '../../lib/financeUi'
import { getSettings, patchSettings } from '../../lib/financeApi'
import {
BANK_DETAILS_EXAMPLE,
@@ -29,14 +29,11 @@ export default function SettingsCard() {
return (
<Card title="Einstellungen">
{res.loading ? (
<div className="empty" role="status">
<FaSpinner className="spinner" aria-hidden="true" />
<p className="muted">Einstellungen werden geladen …</p>
</div>
<div className="loading" role="status">Einstellungen werden geladen …</div>
) : (
<div className="fin-load-error" style={{ padding: 0 }}>
<p className="fin-hint fin-hint-warn" role="alert">{res.error || LOAD_ERROR}</p>
<Button size="sm" onClick={res.reload}>Erneut versuchen</Button>
<div className="fin-load-error">
<p {...noticeProps('warn')}>{res.error || LOAD_ERROR}</p>
<Button onClick={res.reload}>Erneut versuchen</Button>
</div>
)}
</Card>
@@ -103,14 +100,14 @@ function SettingsForm({ initial }) {
<form onSubmit={submit} noValidate>
{/* Während des Speicherns gesperrt, damit die Antwort keine neuen Eingaben überschreibt */}
<fieldset className="fin-fieldset" disabled={busy}>
<div className="fin-form-grid">
<div className="field-row">
<FormField
id={fieldId('minBuffer')}
label="Mindestpuffer (€)"
hint="Dieser Betrag bleibt bei „Verfügbar“ immer auf dem Konto. 0 ist möglich."
error={errors.minBuffer}
>
<input className="form-control fin-input-amount" inputMode="decimal" autoComplete="off" value={form.minBuffer} onChange={change('minBuffer')} placeholder="z. B. 500,00" />
<input className="input fin-input-amount" inputMode="decimal" autoComplete="off" value={form.minBuffer} onChange={change('minBuffer')} placeholder="z. B. 500,00" />
</FormField>
<FormField
id={fieldId('taxReservePercent')}
@@ -118,15 +115,17 @@ function SettingsForm({ initial }) {
hint={`Anteil des Gewinns, der als Rücklage gilt (0 bis ${SETTINGS_LIMITS.maxPercent}).`}
error={errors.taxReservePercent}
>
<input className="form-control fin-input-amount" inputMode="numeric" autoComplete="off" value={form.taxReservePercent} onChange={change('taxReservePercent')} placeholder="30" />
<input className="input fin-input-amount" inputMode="numeric" autoComplete="off" value={form.taxReservePercent} onChange={change('taxReservePercent')} placeholder="30" />
</FormField>
</div>
<div className="field-row">
<FormField
id={fieldId('previousYearRevenue')}
label="Vorjahresumsatz (€)"
hint="Für die Kleinunternehmer-Grenze. Leer lassen, wenn unbekannt."
error={errors.previousYearRevenue}
>
<input className="form-control fin-input-amount" inputMode="decimal" autoComplete="off" value={form.previousYearRevenue} onChange={change('previousYearRevenue')} placeholder="leer = unbekannt" />
<input className="input fin-input-amount" inputMode="decimal" autoComplete="off" value={form.previousYearRevenue} onChange={change('previousYearRevenue')} placeholder="leer = unbekannt" />
</FormField>
<FormField
id={fieldId('estPrepayment')}
@@ -134,7 +133,7 @@ function SettingsForm({ initial }) {
hint="Betrag je Termin (10.03., 10.06., 10.09., 10.12.). 0 = keine Vorauszahlung."
error={errors.estPrepayment}
>
<input className="form-control fin-input-amount" inputMode="decimal" autoComplete="off" value={form.estPrepayment} onChange={change('estPrepayment')} placeholder="0,00" />
<input className="input fin-input-amount" inputMode="decimal" autoComplete="off" value={form.estPrepayment} onChange={change('estPrepayment')} placeholder="0,00" />
</FormField>
</div>
@@ -144,21 +143,17 @@ function SettingsForm({ initial }) {
hint={`Steht im Fuß jeder Rechnung, z. B. „${BANK_DETAILS_EXAMPLE}“.`}
error={errors.bankDetailsText}
>
<textarea className="form-control" rows={3} value={form.bankDetailsText} onChange={change('bankDetailsText')} />
<textarea className="textarea" rows={3} value={form.bankDetailsText} onChange={change('bankDetailsText')} />
</FormField>
<div className={`fin-counter${tooLong ? ' fin-counter-over' : ''}`}>
{length} / {SETTINGS_LIMITS.maxBankDetails} Zeichen
</div>
</fieldset>
{notice && (
<p className={`fin-hint fin-form-notice${notice.tone === 'warn' ? ' fin-hint-warn' : ''}`} role={notice.tone === 'warn' ? 'alert' : 'status'}>
{notice.text}
</p>
)}
{notice && <p {...noticeProps(notice.tone, 'fin-notice')}>{notice.text}</p>}
<div className="fin-form-actions">
<Button type="submit" variant="primary" disabled={busy}>
{busy ? <FaSpinner className="spinner" aria-hidden="true" /> : <FaFloppyDisk aria-hidden="true" />} {busy ? 'Speichere …' : 'Speichern'}
{busy ? 'Speichere …' : 'Speichern'}
</Button>
</div>
</form>
+6 -9
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { PageHeader, Card, Button } from '../components/ui'
import { PageHeader, Button } from '../components/ui'
import { completeBankAuth } from '../lib/financeApi'
import { dateDe } from '../lib/financeFormat'
import { parseCallback, successResult, failureResult, isResult } from '../lib/bankCallback'
@@ -38,28 +38,25 @@ export default function BankCallbackPage() {
if (result) {
body = (
<>
<p role={result.ok ? 'status' : 'alert'} style={{ fontWeight: 600 }}>
<p className={`msg ${result.ok ? 'ok' : 'err'} fin-result`} role={result.ok ? 'status' : 'alert'}>
{result.ok
? `Bankzugang erneuert, gültig bis ${dateDe(result.validUntil)}`
: result.message}
</p>
<div style={{ marginTop: 16 }}>
<div className="fin-form-actions">
<Button as={Link} variant="primary" to="/finance">Zu den Finanzen</Button>
</div>
</>
)
} else if (params) {
body = (
<div className="text-center" role="status">
<div className="spinner" />
<p className="muted">Bankzugang wird eingerichtet …</p>
</div>
<div className="loading" role="status">Bankzugang wird eingerichtet …</div>
)
} else {
body = (
<>
<p className="muted">Zu dieser Seite liegt keine Rückmeldung der Bank vor.</p>
<div style={{ marginTop: 16 }}>
<div className="fin-form-actions">
<Button as={Link} variant="primary" to="/finance">Zu den Finanzen</Button>
</div>
</>
@@ -69,7 +66,7 @@ export default function BankCallbackPage() {
return (
<div className="page">
<PageHeader title="Bankzugang" />
<Card>{body}</Card>
{body}
</div>
)
}
+3 -4
View File
@@ -1,5 +1,4 @@
import { Link } from 'react-router-dom'
import { FaArrowLeft } from 'react-icons/fa6'
import { PageHeader, Button } from '../components/ui'
import SettingsCard from '../components/finance/SettingsCard'
import BankAccessCard from '../components/finance/BankAccessCard'
@@ -7,7 +6,7 @@ import FixedCostsCard from '../components/finance/FixedCostsCard'
/**
* Finanzen / Einstellungen (/finance/einstellungen, nur Admins): Rücklagen und Bankverbindungstext, Bankzugang
* (Status, Abruf, neu verbinden) und die Fixkostenliste. Jede Karte lädt und speichert für sich.
* (Status, Abruf, neu verbinden) und die Fixkostenliste. Jeder Abschnitt lädt und speichert für sich.
*/
export default function FinanceSettingsPage() {
return (
@@ -16,8 +15,8 @@ export default function FinanceSettingsPage() {
title="Finanzen: Einstellungen"
subtitle="Rücklagen, Bankzugang und Fixkosten"
actions={
<Button as={Link} variant="ghost" to="/finance">
<FaArrowLeft aria-hidden="true" /> Zurück zu den Finanzen
<Button as={Link} to="/finance">
← Zurück zu den Finanzen
</Button>
}
/>
+21 -32
View File
@@ -663,9 +663,7 @@ button.link-danger { background: none; border: 0; padding: 0; font: inherit; fon
.fin-block { scroll-margin-top: 68px; } /* unter der 52 px hohen Mobil-Leiste 16 px Luft */
}
/* Uebergang: diese Regeln fallen, sobald keine Datei mehr die Klassen benutzt (Hinweis, Drehknopf) */
.fin-spin { animation: spin 1s linear infinite; }
.fin-hint { margin: 0; font-size: 13px; color: var(--text-muted); }
/* Uebergang: diese Regel faellt, sobald keine Datei die Klasse mehr benutzt (Hinweis im Zuordnen-Dialog) */
.fin-hint-warn { color: var(--warn); }
/* Block 0: Statuszeile (ohne Kasten) */
@@ -892,38 +890,29 @@ button.link-danger { background: none; border: 0; padding: 0; font: inherit; fon
}
/* Finanzen: Einstellungen (Formular, Bankzugang, Fixkosten) */
.fin-fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
.fin-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--sp-4); }
.fin-fieldset { display: grid; gap: 16px; border: 0; padding: 0; margin: 0; min-width: 0; }
.fin-fieldset .field-row { gap: 16px; }
.fin-form-field { min-width: 0; }
.fin-form-hint { font-size: 12px; overflow-wrap: anywhere; }
.fin-field-error { font-size: 13px; font-weight: 600; color: var(--danger); overflow-wrap: anywhere; }
/* KOMPAT setzt .page .field auf 12 px Abstand unten; im Raster bestimmt allein der gap den Abstand */
.page .fin-fieldset .field, .page .fin-fixed-form .field { margin-bottom: 0; }
.fin-input-amount { text-align: right; font-variant-numeric: tabular-nums; }
.fin-counter { margin: -6px 0 var(--sp-3); text-align: right; font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.fin-counter-over { color: var(--danger); font-weight: 700; }
.fin-form-notice { margin: 0 0 var(--sp-3); }
.fin-form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
.fin-bank-facts { display: grid; gap: 6px; margin: 0 0 var(--sp-3); }
.fin-bank-facts > div { display: grid; grid-template-columns: 170px minmax(0, 1fr); gap: var(--sp-3); align-items: baseline; }
.fin-bank-facts dt { color: var(--text-muted); font-size: 13px; }
.fin-bank-facts dd { margin: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.fin-fixed-form { margin-bottom: var(--sp-4); padding: var(--sp-4); border: 1px solid var(--border-strong); border-radius: var(--r-sm); }
.fin-fixed-form-title { margin: 0 0 var(--sp-3); font-size: 15px; font-weight: 700; }
.fin-fixed-list { list-style: none; margin: 0; padding: 0; }
.fin-fixed {
display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px var(--sp-4); align-items: baseline;
padding: var(--sp-3) 0; border-bottom: 1px solid var(--border);
}
.fin-fixed:last-child { border-bottom: none; }
.fin-fixed-off .fin-fixed-name, .fin-fixed-off .fin-fixed-amount { color: var(--text-muted); }
.fin-fixed-main { display: flex; flex-wrap: wrap; align-items: center; gap: 6px var(--sp-2); min-width: 0; }
.fin-fixed-name { font-weight: 700; overflow-wrap: anywhere; }
.fin-fixed-amount { font-weight: 700; }
.fin-fixed-meta { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 2px var(--sp-4); font-size: 13px; overflow-wrap: anywhere; }
.fin-counter { margin-top: -8px; text-align: right; font-size: 12px; color: var(--text-2); font-variant-numeric: tabular-nums; }
.fin-counter-over { color: var(--danger); font-weight: 600; }
.fin-form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 16px; }
.fin-kv { margin: 0 0 16px; }
.fin-result { font-size: 16px; line-height: 24px; font-weight: 600; }
.fin-fixed-form { padding-bottom: 24px; margin-bottom: 8px; border-bottom: 1px solid var(--line); }
.fin-form-title { margin: 0 0 16px; font-size: 14px; line-height: 20px; font-weight: 600; }
.fin-fixed-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
.fin-fixed-form ~ .fin-fixed-list { border-top: 0; } /* das offene Formular schliesst mit seiner eigenen Linie ab */
.fin-fixed { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 16px; align-items: baseline; padding: 12px 0; border-bottom: 1px solid var(--line); }
.fin-fixed-off .fin-fixed-name, .fin-fixed-off .fin-fixed-amount { color: var(--text-2); }
.fin-fixed-main { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; min-width: 0; }
.fin-fixed-name { font-weight: 500; overflow-wrap: anywhere; }
.fin-fixed-amount { font-weight: 600; }
.fin-fixed-meta { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 2px 16px; font-size: 13px; overflow-wrap: anywhere; }
.fin-fixed-actions { grid-column: 1 / -1; padding-top: 4px; }
@media (max-width: 640px) {
.fin-form-grid { grid-template-columns: minmax(0, 1fr); }
.fin-bank-facts > div { grid-template-columns: minmax(0, 1fr); gap: 2px; }
}
.fin-fixed-actions button:disabled, .fin-notice button:disabled { cursor: not-allowed; }
/* ===== ENDE FINANZEN ===== */