Files
LibreLedger/styles.css
T
LibrePortal 2cffc3ab07 Start LibreLedger: an encrypted money ledger that stores only ciphertext
LibreLedger is a single-person money ledger. The browser encrypts
everything (PBKDF2-HMAC-SHA256 with 250k iterations, then AES-256-GCM),
and a small standard-library Python server stores only the resulting
{v, salt, iv, ct} blob. Ledger files from the earlier Money Ledger version
open unchanged.

- On plain http to a LAN or VPN address, where browsers hide Web Crypto,
  the app switches to vendored @noble/hashes and @noble/ciphers 2.2.0 and
  says so on the lock screen. tests/crypto-interop.test.mjs shows both
  paths read each other's files.
- The server hands out only the app's own files, sends a self-only CSP,
  nosniff, frame denial and no-referrer, checks the shape of each blob,
  refuses cross-site writes and writes atomically. It keeps the last 10
  backups plus one per day for 30 days.
- The container runs that server from a digest-pinned python alpine
  image, as an unprivileged user, with its data in /data and a health
  check on /api/health.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 01:17:16 +01:00

1218 lines
60 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.
/* ============================================================
LibreLedger — design system
Dark by default, light on tap. Playful + colourful.
============================================================ */
:root {
--radius: 20px;
--radius-sm: 14px;
--radius-xs: 10px;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
"Apple Color Emoji", "Segoe UI Emoji", sans-serif;
--mono: "SF Mono", ui-monospace, "JetBrains Mono", "Roboto Mono", Menlo, Consolas, monospace;
--ease-bounce: cubic-bezier(.34, 1.56, .64, 1);
}
/* ---- dark theme (default) ---- */
:root,
[data-theme="dark"] {
--bg-1: #0e1020;
--bg-2: #07080f;
--surface: #161a2c;
--surface-2: #1d2238;
--surface-3: #262c47;
--border: #2c3350;
--border-strong: #3b4368;
--ink: #eaf0ff;
--muted: #9aa6cc;
--faint: #6b76a0;
--brand-1: #818cf8;
--brand-2: #c084fc;
--brand-ink: #b6bcff;
--brand-grad: linear-gradient(125deg, #3730a3 0%, #4f46e5 24%, #6d28d9 48%, #8b5cf6 72%, #c026d3 100%);
--in: #34e0a1;
--in-bg: rgba(52, 224, 161, .14);
--in-edge: #22d3a0;
--out-ink: #ff8d6b;
--out-bg: rgba(251, 146, 60, .15);
--out-edge: #fb923c;
--neg: #ff7a8a;
--ring: rgba(168, 85, 247, .50);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, .45);
--shadow-md: 0 14px 30px -12px rgba(0, 0, 0, .6);
--shadow-lg: 0 32px 72px -18px rgba(0, 0, 0, .75);
color-scheme: dark;
}
/* ---- light theme ---- */
[data-theme="light"] {
--bg-1: #f3f1fb;
--bg-2: #e9e7f7;
--surface: #ffffff;
--surface-2: #f8f6ff;
--surface-3: #efeafc;
--border: #e7e2f5;
--border-strong: #d6cfee;
--ink: #1b1633;
--muted: #6a6388;
--faint: #9a93b8;
--brand-1: #6d4cf0;
--brand-2: #c026d3;
--brand-ink: #7c3aed;
--brand-grad: linear-gradient(125deg, #3730a3 0%, #4f46e5 24%, #6d28d9 48%, #8b5cf6 72%, #c026d3 100%);
--in: #0fa968;
--in-bg: rgba(16, 200, 120, .12);
--in-edge: #10b981;
--out-ink: #ea580c;
--out-bg: rgba(251, 146, 60, .14);
--out-edge: #fb923c;
--neg: #e11d48;
--ring: rgba(168, 85, 247, .35);
--shadow-sm: 0 1px 2px rgba(60, 40, 110, .08), 0 1px 3px rgba(60, 40, 110, .06);
--shadow-md: 0 14px 30px -12px rgba(80, 50, 140, .22), 0 4px 10px -4px rgba(80, 50, 140, .12);
--shadow-lg: 0 30px 70px -18px rgba(60, 40, 110, .35);
color-scheme: light;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
min-height: 100vh;
font: 14px/1.5 var(--font);
color: var(--ink);
background:
radial-gradient(1100px 650px at 100% -8%, color-mix(in srgb, #ec4899 18%, transparent), transparent 60%),
radial-gradient(1000px 600px at -8% 4%, color-mix(in srgb, var(--brand-1) 20%, transparent), transparent 55%),
radial-gradient(900px 620px at 55% 112%, color-mix(in srgb, var(--brand-2) 16%, transparent), transparent 60%),
linear-gradient(180deg, var(--bg-1), var(--bg-2));
background-attachment: fixed;
}
.hidden { display: none !important; }
/* ---- brand mark ---- */
.brand { display: flex; align-items: center; gap: 11px; }
.brand-mark {
display: grid; place-items: center;
width: 36px; height: 36px; border-radius: 12px;
font-size: 19px; font-weight: 800; color: #fff;
background: var(--brand-grad) no-repeat; background-size: 220% 220%;
box-shadow: 0 6px 18px -4px color-mix(in srgb, var(--brand-2) 65%, transparent);
animation: shimmer 7s ease-in-out infinite;
transition: transform .3s var(--ease-bounce);
}
.brand:hover .brand-mark { transform: rotate(-8deg) scale(1.08); }
.brand-name {
font-size: 17px; font-weight: 800; letter-spacing: -.01em;
background: var(--brand-grad) no-repeat; background-size: 220% auto;
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; color: transparent;
animation: shimmer 7s ease-in-out infinite;
}
.brand-lg { justify-content: center; flex-direction: column; gap: 12px; }
.brand-lg .brand-mark { width: 52px; height: 52px; font-size: 27px; border-radius: 16px; }
.brand-lg .brand-name { font-size: 23px; }
/* ---- header ---- */
header {
position: sticky; top: 0; z-index: 20;
display: flex; align-items: center; justify-content: space-between;
gap: 16px; padding: 13px 22px;
background: color-mix(in srgb, var(--surface) 72%, transparent);
-webkit-backdrop-filter: saturate(1.6) blur(16px);
backdrop-filter: saturate(1.6) blur(16px);
border-bottom: 1px solid var(--border);
}
.tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tools .cur {
display: inline-flex; align-items: center; gap: 6px;
color: var(--muted); font-size: 13px; font-weight: 500;
}
.tools .cur input {
width: 46px; text-align: center; color: var(--ink);
border: 1px solid var(--border-strong); border-radius: var(--radius-xs);
background: var(--surface); padding: 6px;
}
/* ---- buttons ---- */
button {
font: inherit; font-weight: 600; cursor: pointer;
border: 1px solid var(--border-strong);
background: var(--surface); color: var(--ink);
border-radius: var(--radius-xs); padding: 7px 13px;
transition: background .18s, border-color .18s, color .18s, box-shadow .18s,
filter .18s, transform .2s var(--ease-bounce), background-position .6s;
}
button:hover { background: var(--surface-3); border-color: var(--faint); transform: translateY(-1px); }
button:active { transform: translateY(0) scale(.96); }
button:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
button.primary {
color: #fff; border-color: transparent;
background: var(--brand-grad) no-repeat; background-size: 220% auto;
box-shadow: 0 8px 22px -6px color-mix(in srgb, var(--brand-2) 70%, transparent);
}
button.primary:hover {
filter: brightness(1.08) saturate(1.12); border-color: transparent;
transform: translateY(-1px); background-position: right center;
}
.icon-btn {
width: 36px; height: 34px; padding: 0; font-size: 16px; line-height: 1;
display: grid; place-items: center;
}
.icon-btn:hover { transform: rotate(25deg) scale(1.14); }
/* ---- layout ---- */
main { max-width: 1080px; margin: 28px auto; padding: 0 22px 96px; }
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
padding: 20px 22px;
margin-bottom: 26px;
}
.panel-head { margin-bottom: 16px; position: relative; padding-right: 38px; }
.panel-head h2 {
margin: 0; font-size: 12px; font-weight: 800;
text-transform: uppercase; letter-spacing: .1em;
background: var(--brand-grad) no-repeat; background-size: 200% auto;
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; color: var(--brand-ink);
}
.panel-sub { margin: 5px 0 0; color: var(--muted); font-size: 13px; }
/* ---- tables ---- */
table.grid { width: 100%; border-collapse: separate; border-spacing: 0; }
table.grid thead th {
background: var(--surface-2);
padding: 9px 12px; font-size: 11px; font-weight: 800;
color: var(--faint); text-transform: uppercase; letter-spacing: .06em;
text-align: left; border-bottom: 1px solid var(--border);
}
table.grid th.num, table.grid td.num { text-align: right; }
table.grid tbody td {
padding: 0; border-bottom: 1px solid var(--border);
vertical-align: middle;
}
table.grid tbody tr:last-child td { border-bottom: 0; }
table.grid td input {
width: 100%; border: 0; background: transparent;
font: inherit; color: inherit; padding: 9px 12px; border-radius: var(--radius-xs);
}
table.grid td input::placeholder { color: var(--faint); }
table.grid td input:focus {
outline: none; background: var(--surface);
box-shadow: inset 0 0 0 2px var(--ring);
}
table.grid td input.amt { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
input.amt.in:not(:placeholder-shown) { color: var(--in); font-weight: 700; }
input.amt.out:not(:placeholder-shown) { color: var(--out-ink); font-weight: 700; }
/* hide the clunky native number spinners — replaced by custom steppers */
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; appearance: none; margin: 0; }
/* custom rounded ▲/▼ stepper on money fields */
.stepper { display: inline-flex; align-items: center; gap: 8px; }
.stepper > input.amt { width: 84px; flex: none; padding-right: 4px; text-align: right; }
.spin {
display: inline-flex; flex-direction: column; flex: none;
border: 1px solid var(--border-strong); border-radius: 7px; overflow: hidden;
opacity: .4; transition: opacity .15s;
}
.stepper:hover .spin, .stepper:focus-within .spin { opacity: 1; }
.step {
width: 22px; height: 13px; padding: 0; margin: 0; border: 0; border-radius: 0;
background: var(--surface-3); color: var(--muted);
font-size: 9px; line-height: 1; display: grid; place-items: center;
transition: background .12s, color .12s, transform .1s var(--ease-bounce);
}
.step[data-dir="up"] { border-bottom: 1px solid var(--border-strong); }
.step:hover { background: var(--brand-1); color: #fff; transform: none; }
.step:active { transform: scale(.84); }
input[type="date"] { font-family: var(--mono); color: var(--muted); }
input[type="date"]::-webkit-calendar-picker-indicator { opacity: .5; cursor: pointer; }
td.bal {
padding: 9px 12px; font-family: var(--mono);
font-variant-numeric: tabular-nums; font-weight: 700;
}
td.bal.neg, .num.neg { color: var(--neg); }
/* table footer / totals */
tfoot th {
background: var(--surface-2); padding: 11px 12px;
font-family: var(--mono); font-variant-numeric: tabular-nums;
border-top: 1.5px solid var(--border-strong);
}
tfoot th[colspan] { font-family: var(--font); }
tfoot .total { font-size: 15px; color: var(--ink); }
.grand-total {
font-size: 17px; font-weight: 800;
background: var(--brand-grad) no-repeat; background-size: 200% auto;
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent;
}
/* a negative grand total should still read as red, not gradient */
.grand-total.neg { -webkit-text-fill-color: var(--neg); background: none; color: var(--neg); }
/* hover */
table.grid tbody tr { transition: background .15s, transform .18s var(--ease-bounce); }
table.grid tbody tr:hover { background: var(--surface-2); }
/* ---- ledger row colour coding (Money / Cost) ---- */
tr.led-row td:first-child { position: relative; }
tr.led-row.is-in { background: var(--in-bg); }
tr.led-row.is-out { background: var(--out-bg); }
tr.led-row.is-in:hover { background: color-mix(in srgb, var(--in-bg) 65%, var(--surface-2)); }
tr.led-row.is-out:hover { background: color-mix(in srgb, var(--out-bg) 65%, var(--surface-2)); }
tr.led-row:hover { transform: translateX(3px); }
tr.led-row.is-in td:first-child::before,
tr.led-row.is-out td:first-child::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
transition: width .18s var(--ease-bounce);
}
tr.led-row.is-in td:first-child::before { background: var(--in-edge); }
tr.led-row.is-out td:first-child::before { background: var(--out-edge); }
tr.led-row:hover td:first-child::before { width: 5px; }
/* locked rows (driven by a recurring item — edit in the Recurring section) */
tr.led-row.locked input { cursor: not-allowed; }
tr.led-row.locked input:focus { box-shadow: none; background: transparent; }
tr.led-row.locked .spin { visibility: hidden; } /* hide steppers but keep their space so numbers stay aligned */
.lock-mark { display: inline-block; font-size: 12px; opacity: .55; cursor: default; padding: 5px 6px; }
.tagdot:disabled { cursor: default; }
.tagdot:disabled:hover { transform: none; }
/* per-category summary chips at the top of each month */
.month-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 16px 4px; }
.month-chips:empty { display: none; }
.mchip {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 700;
font-variant-numeric: tabular-nums; white-space: nowrap;
color: var(--muted); background: var(--surface-3); border: 1px solid var(--border);
cursor: pointer; transition: transform .15s var(--ease-bounce), box-shadow .15s, filter .15s, opacity .15s;
}
.mchip.in { color: var(--in); background: var(--in-bg); border-color: color-mix(in srgb, var(--in-edge) 40%, transparent); }
.mchip.out { color: var(--out-ink); background: var(--out-bg); border-color: color-mix(in srgb, var(--out-edge) 45%, transparent); }
.mchip-nm { color: var(--ink); font-weight: 700; }
.mchip-amt { font-weight: 800; }
/* clickable category filter chips */
.mchip:hover { transform: translateY(-1px); filter: brightness(1.06); }
.mchip:active { transform: translateY(0) scale(.97); }
.mchip:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
/* a selected chip gets a clear ring; un-selected chips dim while a filter is active */
.month-chips.filtering .mchip:not(.active) { opacity: .5; }
.mchip.active { box-shadow: 0 0 0 2px var(--brand-1); filter: none; }
.mchip-all { color: var(--ink); background: var(--surface-3); border-color: var(--border-strong); }
.mchip-all.active { box-shadow: 0 0 0 2px var(--brand-1); border-color: var(--brand-1); }
/* "All" toggled off — every category hidden; click it (or any chip) to bring rows back. */
.month-chips.filtering .mchip-all.off { opacity: .9; border-style: dashed; color: var(--muted); }
/* rows / groups hidden by the active category filter */
tr.led-row.filtered-out, .grp.filtered-out { display: none; }
/* months view toggle (chronological ⟷ grouped) */
.months-view { display: flex; justify-content: flex-end; margin-bottom: 12px; }
.viewtoggle.on { color: var(--brand-ink); border-color: var(--brand-1); background: color-mix(in srgb, var(--brand-1) 10%, transparent); }
/* grouped-by-category view */
.month.grouped .groups { padding: 4px; }
.grp { border-bottom: 1px solid var(--border); }
.grp:last-child { border-bottom: 0; }
.grp-head {
display: flex; align-items: center; gap: 10px; width: 100%;
padding: 11px 12px; background: transparent; border: 0; border-radius: var(--radius-xs);
font: inherit; color: var(--ink); cursor: pointer; text-align: left;
}
.grp-head:hover { background: var(--surface-2); transform: none; }
.grp-caret { color: var(--faint); font-size: 11px; width: 12px; flex: none; }
.grp-ico { font-size: 16px; flex: none; }
.grp-label { font-weight: 700; }
.grp-count { font-size: 11px; font-weight: 700; color: var(--muted); background: var(--surface-3); border-radius: 999px; padding: 1px 8px; }
.grp-sub { margin-left: auto; font-weight: 800; font-variant-numeric: tabular-nums; }
.grp-sub.in { color: var(--in); }
.grp-sub.out { color: var(--out-ink); }
.grp-body { padding: 0 12px 8px 34px; }
.grp-row {
display: flex; align-items: center; gap: 10px; padding: 5px 0;
font-size: 13px; border-top: 1px dashed var(--border);
}
.grp-row:first-child { border-top: 0; }
.grp-day { width: 44px; flex: none; font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--faint); }
.grp-desc { flex: 1; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.grp-amt { flex: none; font-family: var(--mono); font-variant-numeric: tabular-nums; font-weight: 700; }
.grp-amt.in { color: var(--in); }
.grp-amt.out { color: var(--out-ink); }
.grp-empty { padding: 24px 16px; text-align: center; color: var(--muted); font-size: 13px; }
.month-totals {
display: flex; flex-wrap: wrap; align-items: center; gap: 16px;
padding: 12px 16px; background: var(--surface-2); border-top: 1px solid var(--border);
font-variant-numeric: tabular-nums;
}
.month-totals .mt { font-weight: 800; font-size: 14px; color: var(--ink); }
.month-totals .mt b { font-size: 10px; font-weight: 700; color: var(--faint); text-transform: uppercase; letter-spacing: .05em; margin-right: 5px; }
.month-totals .mt.net { color: var(--in); }
.month-totals .mt.end { margin-left: auto; }
.month-totals .neg { color: var(--neg); }
.grouped-note { padding: 10px 16px; font-size: 12px; color: var(--faint); }
/* Category colour dot */
.c-tag { width: 1%; padding-left: 6px !important; }
.tagdot {
width: 16px; height: 16px; padding: 0; border-radius: 50%;
border: 2px solid var(--border-strong);
background: var(--dot, transparent);
transition: transform .25s var(--ease-bounce), box-shadow .15s;
}
.tagdot[data-has="1"] { border-color: transparent; box-shadow: 0 0 0 1px var(--border) inset, 0 0 10px -2px var(--dot); }
.tagdot:hover { transform: scale(1.35) rotate(10deg); }
.tagdot:active { transform: scale(1.5) rotate(-14deg); }
/* when a category is set, the dot becomes an "icon Name" chip tinted by its colour */
.tagdot.has-icon {
width: auto; height: auto; padding: 4px 10px 4px 8px;
display: inline-flex; align-items: center; gap: 5px; max-width: 170px;
border-radius: 999px; border: 1px solid var(--border-strong);
background: var(--surface-2); box-shadow: none;
font-size: 12px; line-height: 1.1;
}
.tagdot.has-icon[data-has="1"] {
border-color: color-mix(in srgb, var(--dot) 45%, transparent);
background: color-mix(in srgb, var(--dot) 12%, transparent);
}
.tagdot.has-icon .cat-emoji { font-size: 14px; flex: none; }
.tagdot.has-icon .cat-name { font-weight: 700; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tagdot.has-icon:hover { transform: translateY(-1px); }
.tagdot.has-icon:active { transform: translateY(0) scale(.97); }
.c-tag:has(.tagdot.has-icon) { padding-right: 6px; }
/* bank logo / badge on accounts */
.acct-row { display: flex; align-items: center; gap: 12px; padding-left: 16px; }
.acct-row input { flex: 1 1 auto; min-width: 0; }
.bankpick { padding: 0; border: 0; background: transparent; flex: none; line-height: 0; box-shadow: none; }
.bankpick:hover { transform: translateY(-1px); background: transparent; border: 0; }
.bank-glyph {
display: inline-flex; align-items: center; justify-content: center;
width: 34px; height: 34px; border-radius: 9px; flex: none;
font-size: 14px; font-weight: 800; overflow: hidden; box-sizing: border-box;
}
.bank-logo { background: #fff; border: 1px solid var(--border); padding: 3px; }
.bank-logo img { width: 100%; height: 100%; object-fit: contain; display: block; }
.bank-badge { color: #fff; background: var(--bk, var(--brand-1)); }
.bank-none { background: var(--surface-3); color: var(--faint); font-weight: 400; font-size: 14px; }
.bank-opt-ico { flex: none; display: inline-flex; }
/* recede recurring (locked) rows so the one-off entries you added stand out */
tr.led-row.locked { opacity: .72; }
tr.led-row.locked:hover { opacity: 1; }
/* ---- action buttons ---- */
td.actions, th.actions { width: 1%; text-align: center; white-space: nowrap; }
button.icon {
border: 0; background: transparent; color: var(--faint);
font-size: 17px; line-height: 1; padding: 5px 9px; border-radius: var(--radius-xs);
}
button.icon:hover { background: var(--surface-3); color: var(--ink); transform: translateY(0) scale(1.15); }
button.icon.del-month {
font-size: 12px; font-weight: 700; color: var(--out-ink);
border: 1px solid color-mix(in srgb, var(--out-ink) 35%, transparent);
padding: 5px 11px; background: transparent;
}
button.icon.del-month:hover { background: color-mix(in srgb, var(--out-ink) 14%, transparent); transform: translateY(-1px); }
button.add {
margin: 12px 0 14px 14px; border-style: dashed; border-color: var(--border-strong);
color: var(--brand-ink); background: transparent; font-weight: 700;
}
button.add:hover { background: color-mix(in srgb, var(--brand-1) 12%, transparent); border-color: var(--brand-1); border-style: solid; }
button.add.big { width: 100%; padding: 12px; margin: 8px 0 0; }
/* ---- card (shared by the balances table and each month block) ---- */
.card,
section.month {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
box-shadow: var(--shadow-sm);
}
.card { overflow: hidden; }
/* section.month keeps overflow visible so its header can stick to the viewport */
section.month { margin-bottom: 20px; }
.month-head {
display: flex; align-items: center; gap: 14px;
padding: 12px 16px;
background: var(--surface-2);
border-bottom: 1px solid var(--border);
/* stick just under the global header while scrolling this month */
position: sticky; top: var(--header-h, 60px); z-index: 12;
border-top-left-radius: var(--radius-sm); border-top-right-radius: var(--radius-sm);
}
/* once pinned to the top, drop the rounded top corners so it reads as a flush bar */
.month-head.stuck {
box-shadow: 0 8px 18px -12px rgba(0, 0, 0, .5);
border-top-left-radius: 0; border-top-right-radius: 0;
}
/* the Date/Type/Description… column headers stick just below the month strip */
section.month table.grid thead th {
position: sticky; top: calc(var(--header-h, 60px) + var(--monthhead-h, 52px)); z-index: 11;
}
/* Past (historic) months: dim the body so settled months recede into the background,
while the header stays crisp — its sticky bar and the "Past" badge stay legible. Hover
or focus a past month to bring it back to full strength for a closer look or a quick
edit. Only the body is dimmed (opacity doesn't touch sticky, so the headers behave). */
section.month.past > :not(.month-head) { opacity: .52; transition: opacity .18s ease; }
section.month.past:hover > :not(.month-head),
section.month.past:focus-within > :not(.month-head) { opacity: 1; }
section.month.past .month-head { background: var(--surface-3); }
.hist-badge {
display: inline-flex; align-items: center; gap: 4px;
font-size: 11px; font-weight: 800; letter-spacing: .03em; text-transform: uppercase;
color: var(--muted); background: var(--surface-3);
border: 1px solid var(--border-strong); border-radius: 999px;
padding: 3px 9px; white-space: nowrap;
}
/* compact day-of-month date cell ("9th") — month/year already live in the header */
td.c-day { white-space: nowrap; }
.day-cell { display: inline-flex; align-items: baseline; gap: 1px; padding-left: 14px; }
.c-day input.day-num {
width: 44px; flex: none; text-align: right;
font-family: var(--mono); font-variant-numeric: tabular-nums;
border: 1px solid transparent; background: transparent; color: var(--ink);
border-radius: var(--radius-xs); padding: 9px 2px;
-moz-appearance: textfield; appearance: textfield;
}
.c-day input.day-num::-webkit-inner-spin-button,
.c-day input.day-num::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.c-day input.day-num:focus { outline: none; background: var(--surface); box-shadow: inset 0 0 0 2px var(--ring); }
.c-day input.day-num::placeholder { color: var(--faint); }
.day-ord { font-size: 10px; font-weight: 700; color: var(--faint); }
.day-cell.ro { color: var(--muted); padding: 9px 2px 9px 14px; }
.day-cell.ro .day-num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.month-title {
font-size: 16px; font-weight: 800; flex: 1; color: var(--ink);
border: 0; border-radius: var(--radius-xs); background: transparent;
padding: 5px 8px; min-width: 0;
}
.month-title::placeholder { color: var(--faint); font-weight: 700; }
.month-title:focus { outline: none; box-shadow: 0 0 0 2px var(--ring); background: var(--surface); }
.opening {
display: inline-flex; align-items: center; gap: 8px;
color: var(--muted); font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: .04em;
}
.opening-carry {
font-family: var(--mono); font-variant-numeric: tabular-nums;
font-size: 14px; font-weight: 800; color: var(--ink);
text-transform: none; letter-spacing: 0;
padding: 4px 11px; border-radius: 999px;
background: var(--surface-3); border: 1px solid var(--border);
}
.opening-carry.neg { color: var(--neg); }
section.month table.grid { width: 100%; }
/* month totals row — made to stand out, with a label per figure */
.net-row th {
background: var(--surface-3);
border-top: 2px solid color-mix(in srgb, var(--brand-1) 55%, transparent);
padding-top: 12px; padding-bottom: 12px; vertical-align: middle;
}
.tot-head {
font-family: var(--font); font-size: 13px; font-weight: 800;
color: var(--ink); text-transform: uppercase; letter-spacing: .04em;
}
.tot-lab {
display: block; margin-bottom: 3px;
font-family: var(--font); font-size: 10px; font-weight: 700;
text-transform: uppercase; letter-spacing: .05em; color: var(--faint);
}
.tot-val { display: block; font-family: var(--mono); font-weight: 800; font-size: 14px; color: var(--ink); }
.tot-val.neg { color: var(--neg); }
.tot-end .tot-val {
display: inline-block; padding: 3px 11px; border-radius: 999px;
background: var(--surface); border: 1px solid var(--border-strong); font-size: 15px;
}
.net-chip {
display: inline-block;
color: var(--in); font-weight: 800;
padding: 2px 9px; border-radius: 999px;
background: var(--in-bg);
}
.net-chip.neg { color: var(--neg); background: color-mix(in srgb, var(--neg) 14%, transparent); }
.sav-chip {
display: inline-block; font-weight: 800;
color: var(--in); background: var(--in-bg);
padding: 2px 9px; border-radius: 999px;
}
.sav-chip.neg { color: var(--neg); background: color-mix(in srgb, var(--neg) 14%, transparent); }
.month-totals .mt.sav { color: var(--in); }
.month-totals .mt.sav .neg { color: var(--neg); }
.month-actions { display: flex; flex-wrap: wrap; align-items: center; }
/* ---------- savings panel ---------- */
.sav-card { padding: 16px; }
.sav-stats {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 18px;
}
@media (max-width: 620px) { .sav-stats { grid-template-columns: repeat(2, 1fr); } }
.sav-stat {
display: flex; flex-direction: column; gap: 5px;
background: var(--surface-2); border: 1px solid var(--border);
border-radius: var(--radius-xs); padding: 11px 13px;
}
.sav-stat-lab {
font-size: 10px; font-weight: 700; letter-spacing: .05em;
text-transform: uppercase; color: var(--faint);
}
.sav-stat-val {
font-family: var(--mono); font-variant-numeric: tabular-nums;
font-weight: 800; font-size: 18px; color: var(--in);
}
.sav-stat-val.neg { color: var(--neg); }
.sav-legend { display: flex; gap: 18px; margin-bottom: 8px; }
.sav-leg {
display: inline-flex; align-items: center; gap: 7px;
font-size: 11px; font-weight: 700; color: var(--muted);
}
.sav-leg::before { content: ""; display: inline-block; }
.sav-leg-bar::before { width: 13px; height: 11px; border-radius: 3px; background: var(--in); }
.sav-leg-line::before { width: 17px; height: 0; border-top: 3px solid var(--brand-1); border-radius: 2px; }
.sav-chart { overflow-x: auto; padding-bottom: 4px; }
.sav-svg { display: block; margin: 0 auto; max-width: 100%; height: auto; }
.sav-base { stroke: var(--border-strong); stroke-width: 1; }
.sav-bar rect { transition: opacity .15s; }
.sav-bar.pos rect { fill: var(--in); }
.sav-bar.neg rect { fill: var(--out-edge); }
.sav-bar:hover rect { opacity: .7; }
.sav-line { stroke: var(--brand-1); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.sav-dot circle { fill: var(--brand-1); stroke: var(--surface); stroke-width: 1.5; }
.sav-xlab { fill: var(--muted); font-size: 11px; font-weight: 600; font-family: var(--font); }
.sav-empty {
padding: 32px 18px; text-align: center; color: var(--muted); font-size: 14px;
background: var(--surface-2); border: 1px dashed var(--border-strong);
border-radius: var(--radius-xs);
}
.sav-empty b { color: var(--ink); }
/* ---------- housing affordability ---------- */
.aff-card { padding: 16px; }
.aff-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 18px; }
@media (max-width: 620px) { .aff-stats { grid-template-columns: repeat(2, 1fr); } }
.aff-stat {
display: flex; flex-direction: column; gap: 5px;
background: var(--surface-2); border: 1px solid var(--border);
border-radius: var(--radius-xs); padding: 11px 13px;
}
.aff-stat-lab { font-size: 10px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--faint); }
.aff-stat-val { font-family: var(--mono); font-variant-numeric: tabular-nums; font-weight: 800; font-size: 18px; color: var(--ink); }
.aff-meter { display: flex; flex-direction: column; gap: 11px; }
/* A guidance gauge: your housing % sits somewhere along a smooth green→red
spectrum, so the colour reads as guidance — there's no hard line you pass
or fail. The track gradient matches affordColor() in app.js, so the needle's
colour always lands exactly on the band beneath it. */
.aff-gauge { position: relative; height: 16px; margin: 24px 0 26px; }
.aff-track {
position: absolute; inset: 0; border-radius: 999px; border: 1px solid var(--border);
background: linear-gradient(to right,
hsl(140 75% 46%) 0%, hsl(140 75% 46%) 25%,
hsl(117 75% 46%) 30%, hsl(93 76% 46%) 35%,
hsl(70 77% 46%) 40%, hsl(47 80% 47%) 45%,
hsl(23 84% 49%) 50%, hsl(0 75% 51%) 55%,
hsl(0 75% 51%) 100%);
}
.aff-guide { position: absolute; top: -4px; bottom: -4px; left: 30%; width: 2px; background: var(--ink); opacity: .45; border-radius: 2px; }
.aff-guide-lab {
position: absolute; top: -15px; left: 50%; transform: translateX(-50%);
font-size: 9px; font-weight: 700; letter-spacing: .03em; color: var(--muted); white-space: nowrap;
}
.aff-needle {
position: absolute; top: 50%; left: 0; width: 18px; height: 18px;
transform: translate(-50%, -50%); border-radius: 50%;
background: var(--aff-c, var(--ink)); border: 2px solid var(--surface);
box-shadow: 0 0 0 1px var(--aff-c, var(--ink)), 0 2px 6px rgba(0, 0, 0, .35);
transition: left .4s var(--ease-bounce), background .35s;
}
.aff-needle-val {
position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
font-family: var(--mono); font-variant-numeric: tabular-nums; font-weight: 800; font-size: 12px;
color: var(--aff-c, var(--ink)); white-space: nowrap;
}
.aff-verdict { font-weight: 800; font-size: 15px; }
.aff-budget { font-size: 13px; color: var(--muted); }
.aff-budget b { color: var(--ink); font-family: var(--mono); }
.aff-note { font-size: 11px; color: var(--faint); font-style: italic; }
.aff-good { color: var(--in); }
.aff-bad { color: var(--neg); }
.aff-empty {
padding: 28px 18px; text-align: center; color: var(--muted); font-size: 14px;
background: var(--surface-2); border: 1px dashed var(--border-strong); border-radius: var(--radius-xs);
}
.aff-empty b { color: var(--ink); }
/* ---- form controls (month picker, selects, recurring "when") ---- */
.month-ym {
border: 1px solid var(--border-strong); background: var(--surface);
color: var(--muted); border-radius: var(--radius-xs); padding: 5px 8px;
font: inherit; font-family: var(--mono); font-size: 13px;
}
.month-ym:focus { outline: none; box-shadow: 0 0 0 2px var(--ring); border-color: var(--brand-1); }
table.grid td select {
margin: 6px 8px; border: 1px solid var(--border-strong); background: var(--surface);
color: var(--ink); border-radius: var(--radius-xs); padding: 6px 8px; font: inherit; cursor: pointer;
}
table.grid td select:focus { outline: none; box-shadow: 0 0 0 2px var(--ring); }
/* recurring table: description flexes, everything else hugs its content */
.c-pick, td.c-when, td.c-amt { width: 1%; white-space: nowrap; }
td.c-amt .stepper { width: auto; }
td.c-amt input.amt { width: 84px; flex: none; }
/* fixed, comfortable Description width (id selector beats the global `td input { width:100% }`) */
#recurring-body .c-desc input { width: 176px; min-width: 0; }
.when {
display: inline-flex; align-items: center; gap: 6px;
color: var(--muted); font-size: 13px; padding: 6px 10px; white-space: nowrap;
}
.when em { font-style: normal; color: var(--faint); font-size: 11px; }
.when input { color: var(--ink); font: inherit; }
.when input:focus { outline: none; box-shadow: 0 0 0 2px var(--ring); border-color: var(--brand-1); }
/* visible rounded outline + width (overrides the global `td input { border:0; background:transparent; width:100% }`) */
.when input.when-num,
.when input.when-date {
flex: none; font-family: var(--mono);
border: 1px solid var(--border-strong); background: var(--surface-2);
border-radius: var(--radius-xs); padding: 5px 7px;
}
.when input.when-num { width: 56px; text-align: center; }
.when input.when-date { width: auto; padding: 5px 8px; }
/* compact, always-visible steppers on the when number fields */
.when .stepper { width: auto; gap: 5px; }
.when .stepper input.when-num { width: 46px; }
.when .spin { opacity: 1; }
/* weekly end-date (compact-by-default): a small toggle that reveals the "to" field */
.when-addend {
flex: none; border: 1px dashed var(--border-strong); background: transparent;
color: var(--faint); font-size: 11px; font-weight: 700; white-space: nowrap;
padding: 4px 9px; border-radius: 999px;
}
.when-addend:hover {
color: var(--brand-ink); border-color: var(--brand-1); border-style: solid;
background: color-mix(in srgb, var(--brand-1) 10%, transparent); transform: none;
}
.when-end { display: inline-flex; align-items: center; gap: 4px; }
button.icon.when-x { font-size: 13px; padding: 2px 5px; }
/* keep a wide recurring row reachable instead of clipping it */
.hscroll { overflow-x: auto; overflow-y: visible; }
/* recurring pause toggle */
.c-switch { width: 1%; padding-left: 14px !important; padding-right: 16px !important; }
.switch {
position: relative; width: 38px; height: 22px; padding: 0; border-radius: 999px;
border: 1px solid var(--border-strong); background: var(--surface-3); cursor: pointer;
transition: background .18s, border-color .18s, filter .18s;
}
.switch:hover { transform: none; filter: brightness(1.08); }
.switch:active { transform: none; }
.switch .knob {
position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
background: var(--faint); transition: transform .2s var(--ease-bounce), background .18s;
}
.switch.on { background: var(--brand-1); border-color: transparent; }
.switch.on .knob { transform: translateX(16px); background: #fff; }
tr.rec-off td:not(.c-switch) { opacity: .5; }
tr.rec-off .c-desc input { text-decoration: line-through; }
/* drag-and-drop reorder */
.c-drag { width: 1%; padding-left: 12px !important; }
.drag-handle {
display: inline-block; cursor: grab; color: var(--faint);
font-size: 15px; line-height: 1; padding: 4px 2px; user-select: none;
}
.drag-handle:hover { color: var(--ink); }
.drag-handle:active { cursor: grabbing; }
tr.dragging { opacity: .45; }
tr.drop-before td { box-shadow: inset 0 2px 0 0 var(--brand-1); }
tr.drop-after td { box-shadow: inset 0 -2px 0 0 var(--brand-1); }
/* reorder buttons — ↑/↓/× sit inline at the end of the row */
.c-move { white-space: nowrap; }
/* let the recurring table size to its content so Description stops stretching to fill */
#recurring-body table.grid { width: auto; }
button.icon.move { font-size: 14px; padding: 4px 5px; }
button.icon.move:disabled { opacity: .25; cursor: default; }
button.icon.move:disabled:hover { background: transparent; color: var(--faint); transform: none; }
td.empty { color: var(--muted); text-align: center; padding: 22px 16px !important; font-size: 13px; }
/* footers: recurring actions + "generate N months ahead" */
.rec-foot { display: flex; flex-wrap: wrap; align-items: center; }
.months-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.months-foot .add.big { flex: 1 1 240px; width: auto; margin: 0; }
.bulk {
display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;
color: var(--muted); font-size: 13px;
}
.bulk .add, .bulk input { margin: 0; }
/* ---- category / bank option buttons (shared by the picker modals) ---- */
.cat-opt {
display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
padding: 7px 9px; border: 1px solid transparent; border-radius: 9px;
background: transparent; color: var(--ink); font: inherit; font-size: 13px; cursor: pointer;
}
.cat-opt:hover { background: var(--surface-2); transform: none; }
.cat-opt.active { border-color: var(--brand-1); background: color-mix(in srgb, var(--brand-1) 12%, transparent); }
.cat-opt .cat-emoji { font-size: 16px; flex: none; width: 20px; text-align: center; }
.cat-opt .cat-nm { flex: 1; font-weight: 600; }
.cat-opt .cat-sw { flex: none; width: 12px; height: 12px; border-radius: 50%; background: var(--dot, transparent); box-shadow: 0 0 0 1px var(--border) inset; }
.cat-opt.none { color: var(--muted); font-weight: 700; }
.pop-foot { margin-top: 9px; padding-top: 10px; border-top: 1px solid var(--border); }
.pop-foot-lab { display: block; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--faint); margin-bottom: 7px; }
.pop-swatches { display: grid; grid-template-columns: repeat(8, 1fr); gap: 7px; }
.tag-modal .swatch {
width: 26px; height: 26px; padding: 0; border-radius: 50%; cursor: pointer;
background: var(--dot); border: 2px solid var(--border-strong);
box-shadow: 0 0 9px -2px var(--dot);
transition: transform .12s var(--ease-bounce);
}
.tag-modal .swatch:hover { transform: scale(1.22); }
.tag-modal .swatch.active { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--brand-1); }
.tag-modal .swatch[data-color=""] {
box-shadow: none;
background:
linear-gradient(45deg, transparent 45%, var(--neg) 45%, var(--neg) 55%, transparent 55%),
var(--surface-2);
}
@keyframes pop-in { from { opacity: 0; transform: scale(.9); } to { opacity: 1; transform: scale(1); } }
/* ---- schedule chip + popover (replaces the wide inline Frequency/When fields) ---- */
td.c-when { white-space: nowrap; }
.when-chip {
display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
margin-left: 12px; padding: 7px 11px; border-radius: 999px;
border: 1px solid var(--border-strong); background: var(--surface-2);
color: var(--ink); font-weight: 600; font-size: 12.5px; white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.when-chip:hover {
border-color: var(--brand-1); color: var(--brand-ink);
background: color-mix(in srgb, var(--brand-1) 10%, transparent);
}
.sched-pop {
position: fixed; z-index: 200; width: 252px;
display: flex; flex-direction: column; gap: 9px;
padding: 14px; border-radius: 14px;
background: var(--surface); border: 1px solid var(--border);
box-shadow: var(--shadow-lg);
}
.sched-pop:not(.hidden) { animation: pop-in .12s var(--ease-bounce); }
.sched-title { font-weight: 800; font-size: 13px; color: var(--ink); }
.sched-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; color: var(--muted); }
.sched-row > span { flex: none; }
.sched-row em { font-style: normal; color: var(--faint); font-size: 11px; }
.sched-row input, .sched-row select {
flex: 1 1 auto; min-width: 0; width: auto; text-align: left;
border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--ink);
border-radius: var(--radius-xs); padding: 7px 9px; font: inherit; font-size: 13px;
}
.sched-row input:focus, .sched-row select:focus { outline: none; border-color: var(--brand-1); box-shadow: 0 0 0 2px var(--ring); }
.sched-foot { display: flex; justify-content: flex-end; margin-top: 2px; }
.sched-foot button { padding: 7px 16px; }
/* ---- lock screen ---- */
.overlay {
position: fixed; inset: 0; z-index: 100;
display: flex; align-items: center; justify-content: center;
padding: 20px;
background:
radial-gradient(900px 600px at 80% -10%, color-mix(in srgb, #ec4899 45%, transparent), transparent 60%),
radial-gradient(800px 600px at 0% 110%, color-mix(in srgb, var(--brand-1) 40%, transparent), transparent 55%),
radial-gradient(700px 500px at 100% 100%, color-mix(in srgb, var(--brand-2) 35%, transparent), transparent 60%),
linear-gradient(160deg, var(--bg-1), var(--bg-2));
}
.lock-box {
background: color-mix(in srgb, var(--surface) 88%, transparent);
-webkit-backdrop-filter: blur(22px); backdrop-filter: blur(22px);
border: 1px solid var(--border);
border-radius: 24px; padding: 34px 30px;
width: 344px; display: flex; flex-direction: column; gap: 14px;
box-shadow: var(--shadow-lg);
}
.lock-box p { margin: 0; color: var(--muted); text-align: center; font-size: 13px; }
.lock-box input {
border: 1px solid var(--border-strong); background: var(--surface);
color: var(--ink); border-radius: var(--radius-sm);
padding: 12px 14px; font: inherit;
}
.lock-box input:focus { outline: none; box-shadow: 0 0 0 3px var(--ring); border-color: var(--brand-1); }
.lock-box button {
color: #fff; border-color: transparent; padding: 13px; font-weight: 800; font-size: 15px;
background: var(--brand-grad) no-repeat; background-size: 220% auto;
box-shadow: 0 10px 24px -8px color-mix(in srgb, var(--brand-2) 70%, transparent);
}
.lock-box button:hover { filter: brightness(1.08) saturate(1.12); background-position: right center; transform: translateY(-1px); }
.err { color: var(--neg) !important; min-height: 16px; text-align: center; font-size: 13px; }
.lock-warn { margin: 4px 0 0; padding: 8px 10px; border-radius: 8px; font-size: 12px; line-height: 1.4; text-align: left; color: var(--muted, inherit); border: 1px solid color-mix(in srgb, #f59e0b 45%, transparent); background: color-mix(in srgb, #f59e0b 10%, transparent); }
/* ---- budget tabs (spreadsheet-style) ---- */
.budget-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 22px; }
.budget-tabs { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
.budget-tab {
padding: 8px 14px; border-radius: 999px;
border: 1px solid var(--border-strong); background: var(--surface);
color: var(--muted); font-weight: 700; font-size: 13px;
max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.budget-tab:hover { color: var(--ink); }
.budget-tab.active {
color: #fff; border-color: transparent;
background: var(--brand-grad) no-repeat; background-size: 220% auto;
box-shadow: 0 8px 22px -10px color-mix(in srgb, var(--brand-2) 70%, transparent);
}
.budget-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
/* Fixed height so emoji-prefixed labels (✎ ⧉ 🗑) can't inflate the box — every .bz lines up. */
.bz { height: 32px; padding: 0 12px; font-size: 12.5px; font-weight: 700; display: inline-flex; align-items: center; gap: 5px; line-height: 1; }
.bz.add { margin: 0; border-style: dashed; color: var(--brand-ink); } /* cancel button.add's standalone margins so it sits inline */
.bz.add:hover { border-style: solid; border-color: var(--brand-1); background: color-mix(in srgb, var(--brand-1) 12%, transparent); }
.bz.danger { color: var(--neg); border-color: color-mix(in srgb, var(--neg) 35%, transparent); }
.bz.danger:hover { color: var(--neg); border-color: var(--neg); background: color-mix(in srgb, var(--neg) 12%, transparent); }
/* ---- section nav: tabs (focus one) + show/hide chips (in the All view) ---- */
.section-nav { display: flex; flex-direction: column; gap: 10px; margin-bottom: 22px; }
.sec-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
.sec-tab {
display: inline-flex; align-items: center; gap: 6px; line-height: 1;
padding: 9px 15px; border-radius: 999px; font-size: 13px; font-weight: 700;
border: 1px solid var(--border-strong); background: var(--surface); color: var(--muted);
}
.sec-tab:hover { color: var(--ink); }
.sec-tab.active {
color: #fff; border-color: transparent;
background: var(--brand-grad) no-repeat; background-size: 220% auto;
box-shadow: 0 8px 22px -10px color-mix(in srgb, var(--brand-2) 70%, transparent);
}
.section-hidden { display: none !important; }
/* "Hidden" restore strip — appears only in the All view when sections are hidden */
.sec-hidden { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.sec-hidden[hidden] { display: none; }
.sec-hidden-lab { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--faint); margin-right: 3px; }
.sec-restore {
display: inline-flex; align-items: center; gap: 6px; line-height: 1;
padding: 6px 11px; border-radius: 999px; font-size: 12px; font-weight: 700;
border: 1px dashed var(--border-strong); background: var(--surface-2); color: var(--muted);
}
.sec-restore:hover { color: var(--ink); border-style: solid; border-color: var(--brand-1); }
/* per-section hide ✕ in the panel header (only while the All view is active) */
.panel-hide {
position: absolute; top: 0; right: 0;
width: 30px; height: 30px; padding: 0; border-radius: 50%;
font-size: 12px; line-height: 1; color: var(--muted);
background: var(--surface-2); border: 1px solid var(--border);
}
.panel-hide:hover { color: var(--neg); border-color: color-mix(in srgb, var(--neg) 40%, transparent); background: var(--surface-3); }
#app .panel-hide { display: none; }
#app.view-all .panel-hide { display: inline-flex; align-items: center; justify-content: center; }
/* ---- header "More" menu ---- */
.menu { position: relative; display: inline-flex; }
.menu-btn { padding: 7px 12px; font-size: 17px; line-height: 1; letter-spacing: .04em; }
.menu.open .menu-btn { border-color: var(--brand-1); color: var(--ink); background: var(--surface-3); }
.menu-pop {
position: absolute; top: calc(100% + 7px); right: 0; z-index: 60;
display: flex; flex-direction: column; gap: 3px; min-width: 190px; padding: 7px;
border-radius: var(--radius-sm); background: var(--surface);
border: 1px solid var(--border-strong); box-shadow: var(--shadow-lg);
animation: pop-in .12s var(--ease-bounce);
}
.menu-pop[hidden] { display: none; }
.menu-pop button {
width: 100%; justify-content: flex-start; text-align: left;
background: transparent; border-color: transparent; font-weight: 600;
}
.menu-pop button:hover { background: var(--surface-2); border-color: transparent; transform: none; }
/* ---- budget chooser modal (Export / Backup) ---- */
.modal-overlay { background: rgba(10, 12, 24, .55); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
.modal {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-sm); box-shadow: var(--shadow-lg);
width: 360px; max-width: 100%; padding: 22px;
display: flex; flex-direction: column; gap: 12px;
}
.modal-title { margin: 0; font-size: 17px; font-weight: 800; color: var(--ink); }
.modal-sub { margin: 0; font-size: 12.5px; color: var(--muted); }
.modal-list { display: flex; flex-direction: column; gap: 2px; max-height: 46vh; overflow: auto; margin: 2px 0; }
.modal-row {
display: flex; align-items: center; gap: 10px; padding: 9px 10px;
border-radius: var(--radius-xs); cursor: pointer; color: var(--ink); font-weight: 600; font-size: 14px;
}
.modal-row:hover { background: var(--surface-2); }
.modal-row input { width: 16px; height: 16px; accent-color: var(--brand-1); flex: none; }
.modal-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.modal-actions { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
.modal-actions button:disabled { opacity: .45; cursor: not-allowed; }
.modal-spacer { flex: 1; }
.modal-allnone { font-size: 12px; padding: 6px 10px; }
/* Grouped chooser (e.g. Export to PDF — budgets + sections in one dialog). */
.modal-group { display: flex; flex-direction: column; gap: 4px; }
.modal-group-head { display: flex; align-items: center; justify-content: space-between; font-size: 11px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; color: var(--faint); }
.modal-group-head .modal-allnone { font-size: 11px; padding: 3px 8px; }
/* The budget list can grow long → cap & scroll it; the 6 fixed sections show in full. */
.modal-list[data-pdf-list="budgets"] { max-height: 22vh; }
/* ---- category & colour picker — centered modal ---- */
.tag-modal {
background:
radial-gradient(720px 520px at 50% -6%, color-mix(in srgb, var(--brand-1) 26%, transparent), transparent 60%),
rgba(8, 10, 22, .62);
-webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
}
.tag-modal:not(.hidden) .tag-modal-box { animation: pop-in .14s var(--ease-bounce); }
.tag-modal-box { width: 540px; max-width: 100%; gap: 13px; }
.tag-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.modal-x {
width: 32px; height: 32px; flex: none; padding: 0; border-radius: 50%;
font-size: 13px; line-height: 1; color: var(--muted);
}
.modal-x:hover { color: var(--ink); }
.tag-search {
width: 100%; box-sizing: border-box;
border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--ink);
border-radius: var(--radius-xs); padding: 11px 14px; font: inherit;
}
.tag-search::placeholder { color: var(--faint); }
.tag-search:focus { outline: none; border-color: var(--brand-1); box-shadow: 0 0 0 3px var(--ring); }
.cat-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(152px, 1fr));
gap: 4px; max-height: 48vh; overflow-y: auto;
padding: 2px; margin: 0 -2px;
}
.cat-grid .cat-opt.none { grid-column: 1 / -1; justify-content: center; }
/* search filtering sets [hidden]; override .cat-opt's display:flex so it actually hides */
.cat-opt[hidden] { display: none; }
.cat-empty { padding: 18px; text-align: center; color: var(--muted); font-size: 13px; }
/* ---- linked save file ---- */
#btn-linkfile.linked {
color: var(--in); border-color: color-mix(in srgb, var(--in) 45%, transparent);
background: color-mix(in srgb, var(--in) 12%, transparent);
}
#btn-linkfile.linked:hover { background: color-mix(in srgb, var(--in) 20%, transparent); border-color: var(--in); }
#btn-linkfile.needs {
color: var(--out-ink); border-color: color-mix(in srgb, var(--out-ink) 50%, transparent);
background: color-mix(in srgb, var(--out-ink) 14%, transparent);
}
#btn-linkfile.needs:hover { background: color-mix(in srgb, var(--out-ink) 22%, transparent); border-color: var(--out-ink); }
/* file actions on the lock screen (overrides the gradient .lock-box button styling) */
.lock-fs { display: flex; flex-direction: column; gap: 8px; }
.lock-fs:empty { display: none; }
.lock-fs:not(:empty) { margin-top: 4px; padding-top: 14px; border-top: 1px solid var(--border); }
.lock-box .lock-fs-btn {
background: var(--surface); color: var(--ink);
border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
padding: 11px; font-weight: 700; font-size: 13px; box-shadow: none;
}
.lock-box .lock-fs-btn:hover { background: var(--surface-3); border-color: var(--faint); transform: translateY(-1px); filter: none; }
.lock-box .lock-fs-btn.primary-soft {
border-color: var(--brand-1); color: var(--brand-ink);
background: color-mix(in srgb, var(--brand-1) 12%, transparent);
}
.lock-box .lock-fs-btn.primary-soft:hover { background: color-mix(in srgb, var(--brand-1) 20%, transparent); }
/* ---- motion ---- */
@keyframes shimmer {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;
}
}
/* ---- totals: annualised stat cards + click-to-chart modal ---- */
.tt-grid {
display: grid; gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(165px, 1fr));
}
.tt-card {
display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
text-align: left; cursor: pointer;
padding: 15px 16px 13px; border-radius: var(--radius-sm);
border: 1px solid var(--border); background: var(--surface-2);
border-left: 3px solid var(--brand-1);
transition: transform .14s var(--ease-bounce), box-shadow .15s, border-color .15s, background .15s;
}
.tt-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); background: var(--surface-3); }
.tt-card:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
.tt-in { border-left-color: var(--in); }
.tt-out { border-left-color: var(--out-edge); }
.tt-net { border-left-color: var(--brand-1); }
.tt-top { display: flex; align-items: center; gap: 7px; margin-bottom: 7px; }
.tt-ic { font-size: 17px; line-height: 1; }
.tt-lab { font-size: 11px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
.tt-year {
font-family: var(--mono); font-variant-numeric: tabular-nums;
font-weight: 800; font-size: 25px; line-height: 1.1; color: var(--ink);
}
.tt-year.neg { color: var(--neg); }
.tt-year-cap { font-size: 10.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); }
.tt-month { margin-top: 8px; font-size: 12.5px; color: var(--muted); }
.tt-month b { font-family: var(--mono); font-variant-numeric: tabular-nums; font-weight: 800; color: var(--ink); }
.tt-month b.neg { color: var(--neg); }
.tt-cta {
margin-top: 11px; font-size: 11px; font-weight: 700; color: var(--brand-ink);
opacity: .8; transition: opacity .15s;
}
.tt-card:hover .tt-cta { opacity: 1; }
.tt-empty { margin: 12px 2px 0; color: var(--muted); font-size: 13px; }
.tt-empty b { color: var(--ink); }
.tt-modal-box { width: auto; min-width: min(360px, 92vw); max-width: min(840px, 96vw); }
.tt-modal:not(.hidden) .tt-modal-box { animation: pop-in .14s var(--ease-bounce); }
.tt-modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.tt-modal-chart { overflow-x: auto; padding: 6px 2px 2px; }
.tt-svg { display: block; margin: 0 auto; max-width: 100%; height: auto; }
.tt-base { stroke: var(--border-strong); stroke-width: 1; }
.tt-bar rect { transition: opacity .15s; }
.tt-bar:hover rect { opacity: .72; }
.tt-svg.tt-in .tt-bar.pos rect { fill: var(--in); }
.tt-svg.tt-in .tt-bar.neg rect { fill: var(--neg); }
.tt-svg.tt-out .tt-bar.pos rect { fill: var(--out-edge); }
.tt-svg.tt-out .tt-bar.neg rect { fill: var(--in); }
.tt-svg.tt-net .tt-bar.pos rect { fill: var(--in); }
.tt-svg.tt-net .tt-bar.neg rect { fill: var(--neg); }
.tt-avg { stroke: var(--brand-1); stroke-width: 1.5; stroke-dasharray: 5 4; opacity: .85; }
.tt-avg-lab { fill: var(--brand-ink); font-size: 11px; font-weight: 700; font-family: var(--font); }
.tt-xlab { fill: var(--muted); font-size: 11px; font-weight: 600; font-family: var(--font); }
.tt-chart-empty {
padding: 30px 16px; text-align: center; color: var(--muted); font-size: 13.5px;
background: var(--surface-2); border-radius: var(--radius-xs);
}
.tt-chart-empty b { color: var(--ink); }
/* ---- responsive ---- */
@media (max-width: 720px) {
header { flex-direction: column; align-items: stretch; gap: 12px; }
.tools { justify-content: flex-start; }
main { padding: 0 14px 80px; }
.panel { padding: 16px; }
.tt-grid { grid-template-columns: repeat(2, 1fr); }
}
/* ---------- PDF / print export ----------
#print-root is built on demand by exportPdf() in app.js — a clean snapshot of
one or more budgets (every section, regardless of the on-screen tab). It is
hidden on screen and is the ONLY thing shown when printing. A light, ink-friendly
palette is scoped to it so the PDF reads as a tidy document even in dark mode.
The `.print-preview` class mirrors these rules on screen for visual checks. */
#print-root { display: none; }
#print-root.print-preview { display: block; }
@media print {
body > *:not(#print-root) { display: none !important; }
#print-root { display: block !important; }
@page { margin: 13mm; }
html, body { background: #fff !important; }
}
/* The look of the report itself — shared by real printing and the .print-preview class. */
.print-preview, #print-root {
--bg-1: #fff; --bg-2: #fff;
--surface: #fff; --surface-2: #faf9ff; --surface-3: #f1edfb;
--border: #e1dcf1; --border-strong: #cdc5e6;
--ink: #1b1633; --muted: #595373; --faint: #837ca0;
--in: #0a8f54; --in-bg: rgba(16, 170, 104, .10); --in-edge: #0a8f54;
--out-ink: #c2410c; --out-bg: rgba(251, 146, 60, .12); --out-edge: #e0762e;
--neg: #c81e44;
color: var(--ink); background: #fff; font-family: var(--font); font-size: 12px;
-webkit-print-color-adjust: exact; print-color-adjust: exact;
padding: 18px;
}
.print-preview *, #print-root * { box-shadow: none !important; text-shadow: none !important; animation: none !important; }
.print-head {
display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
border-bottom: 2px solid var(--ink); padding-bottom: 7px; margin-bottom: 4px;
}
.print-brand { font-weight: 900; font-size: 17px; letter-spacing: -.01em; }
.print-meta { color: var(--muted); font-size: 11px; font-family: var(--mono); }
.print-budget { break-before: page; padding-top: 6px; }
.print-budget:first-of-type { break-before: avoid; }
.print-bname {
font-size: 19px; font-weight: 900; margin: 8px 0 12px;
padding-bottom: 5px; border-bottom: 1px dashed var(--border-strong);
}
/* Every section flows as a plain block and may split across pages — only small
atomic units (rows, stat cards) are kept whole, so there are no big gaps. */
.print-preview .panel, #print-root .panel {
display: block !important;
margin: 0 0 16px; padding: 0; background: none; border: none;
}
.print-preview .panel-head, #print-root .panel-head { margin: 0 0 7px; }
.print-preview .panel-head h2, #print-root .panel-head h2 { font-size: 14px; margin: 0; }
.print-preview .panel-sub, #print-root .panel-sub { font-size: 10.5px; margin: 2px 0 0; }
/* Strip interactive chrome — buttons, steppers, toggles, drag handles, add-rows.
`.spin` (the ▲▼ stepper) and the redundant month picker are dropped too. */
.print-preview .panel-hide, .print-preview .spin, .print-preview .step,
.print-preview .add, .print-preview .icon, .print-preview .bz, .print-preview .switch,
.print-preview .drag-handle, .print-preview .tt-cta, .print-preview .lock-mark,
.print-preview .months-foot, .print-preview .bulk, .print-preview .month-actions, .print-preview .month-ym,
#print-root .panel-hide, #print-root .spin, #print-root .step,
#print-root .add, #print-root .icon, #print-root .bz, #print-root .switch,
#print-root .drag-handle, #print-root .tt-cta, #print-root .lock-mark,
#print-root .months-foot, #print-root .bulk, #print-root .month-actions, #print-root .month-ym,
.print-preview .hist-badge, #print-root .hist-badge { display: none !important; }
/* On paper a settled month is just as real as any other — print it at full strength. */
.print-preview section.month.past > :not(.month-head),
#print-root section.month.past > :not(.month-head) { opacity: 1 !important; }
/* Inputs & selects become static, borderless text. Keep number spinners hidden
the way the app does (`-moz-appearance: textfield`), and drop the select arrow. */
.print-preview input, .print-preview select,
#print-root input, #print-root select {
border: none !important; background: transparent !important; padding: 1px 3px !important;
color: var(--ink) !important; font: inherit; width: auto !important;
}
.print-preview input[type="number"], #print-root input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.print-preview select, #print-root select { -moz-appearance: none; appearance: none; }
.print-preview input.amt, #print-root input.amt { width: auto !important; text-align: right; }
.print-preview input.amt.in, #print-root input.amt.in { color: var(--in) !important; font-weight: 700; }
.print-preview input.amt.out, #print-root input.amt.out { color: var(--out-ink) !important; font-weight: 700; }
.print-preview .stepper, #print-root .stepper { gap: 0; }
/* Cards & tables: light, bordered. Long tables/months flow across pages; the
browser repeats the table header on each page automatically. */
.print-preview .card, #print-root .card { background: var(--surface-2) !important; border: 1px solid var(--border) !important; }
.print-preview table.grid, #print-root table.grid { width: 100%; }
/* Keep ONLY small, bounded things whole — a table row, a stat card, the gauge —
so nothing splits awkwardly, but tall blocks never get shoved to a fresh page. */
.print-preview tr, .print-preview .tt-card, .print-preview .aff-card, .print-preview .sav-card,
#print-root tr, #print-root .tt-card, #print-root .aff-card, #print-root .sav-card { break-inside: avoid; }
/* Don't strand a heading at the foot of a page — keep it with what follows. */
.print-preview .panel-head, .print-preview .print-bname, .print-preview .month-head, .print-preview thead,
#print-root .panel-head, #print-root .print-bname, #print-root .month-head, #print-root thead { break-after: avoid; }
.print-preview .tt-card, #print-root .tt-card { border: 1px solid var(--border) !important; background: var(--surface-2) !important; transform: none !important; }