Gonçalo Rodrigues 2ab3acdce2 feat(goals): Goal Planner — type-driven planner merged into /goals tab
* feat(property): Layer 3 — Dream House Simulator

Add /dream page with a four-phase simulation engine:

  Phase 1 — Save the down payment (uses current property equity)
  Phase 2 — Construction period (both loans running simultaneously)
  Phase 3 — Sell current house, apply proceeds to construction loan
  Phase 4 — Final state: just the construction loan remaining

Inputs: dream cost, down payment %, construction loan rate/term,
build duration, monthly savings, expected sale price. All pre-filled
from existing property/loan data when available.

Output: per-phase timeline cards, monthly cost bar chart, total
interest, final payoff date, and a key levers section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(plan): rename Dream House to Goal Planner at /plan

- Route /dream → /plan
- Nav label "Dream House" → "Goal Planner"
- Template dream.html → plan.html
- All user-facing labels generalised (construction loan → new loan,
  build duration → acquisition/build period, current property →
  current asset, dream house cost → new goal cost, etc.)
- Empty state updated with generic copy and 🎯 icon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(goals): merge Goal Planner into /goals as a second tab

- /goals now has two tabs: "Committed goals" and "Goal Planner"
- Goal creation only happens from the Planner tab (simulate first,
  then "Save as goal" → creates an uncommitted goal)
- Commitment, deadline adjustment, and deletion stay on the Goals tab
- Off-track goals show an "Adjust deadline →" button that pushes the
  deadline to the realistic date based on current savings rate
- /plan and /dream both redirect to /goals?tab=planner (301)
- "Goal Planner" nav link removed; plan.html kept for redirect compat
- GoalsData gains Tab, PlanProperties, PlanLoans, HasPlanResult,
  PlanResult, PlanForm fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(goals): type-driven planner — Save for a purchase vs Sell & upgrade

Goal Planner tab now opens with two goal type cards:

  🛒 Save for a purchase — name, target, monthly savings, optional
     deadline. Shows time-to-reach at current rate, monthly needed
     to hit the deadline, and a feasibility banner.

  🔄 Sell & upgrade — the full four-phase transition simulator
     (existing asset + loan → acquire new → sell old → payoff).

Each type has its own focused form and result section. Selecting a
type highlights the card and loads the matching form. Results include
a "Save as goal" action that drops an uncommitted goal into the
Goals tab.

Also adds runPurchaseSim() and PurchaseSimResult model.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 22:02:41 +01:00

730 lines
30 KiB
HTML
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.

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{if .Title}}{{.Title}} — {{end}}Finance</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
/* ── Tokens ─────────────────────────────────────────────────────── */
:root {
--bg: #080c10;
--bg2: #0d1318;
--bg3: #131c24;
--surface: rgba(13, 22, 32, 0.88);
--surface2: rgba(20, 32, 44, 0.75);
--border: rgba(0,210,200,0.08);
--border2: rgba(0,210,200,0.15);
--text: #dff4f2;
--text2: #7fb8b4;
--text3: #3d6e6a;
--accent: #00c9b8;
--accent2: #33d9ca;
--accent-glow: rgba(0,201,184,0.22);
--green: #00e5b0;
--red: #f87171;
--green-dim: rgba(0,229,176,0.12);
--red-dim: rgba(248,113,113,0.13);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.5), 0 1px 2px rgba(0,0,0,0.4);
--shadow-md: 0 4px 16px rgba(0,0,0,0.6), 0 2px 6px rgba(0,0,0,0.4);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.7), 0 4px 12px rgba(0,0,0,0.5);
--radius: 14px;
--radius-sm: 8px;
--nav-h: 58px;
}
[data-theme="light"] {
--bg: #edf6f5;
--bg2: #dceeed;
--bg3: #cae5e3;
--surface: rgba(255,255,255,0.92);
--surface2: rgba(237,246,245,0.85);
--border: rgba(0,150,140,0.1);
--border2: rgba(0,150,140,0.18);
--text: #0d2422;
--text2: #2a6460;
--text3: #6aadaa;
--accent: #00897b;
--accent2: #00a896;
--accent-glow: rgba(0,137,123,0.15);
--green: #00796b;
--red: #dc2626;
--green-dim: rgba(0,121,107,0.1);
--red-dim: rgba(220,38,38,0.1);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.04);
--shadow-md: 0 4px 16px rgba(0,0,0,0.09), 0 2px 6px rgba(0,0,0,0.05);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.11), 0 4px 12px rgba(0,0,0,0.06);
}
/* ── Reset & base ────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
transition: background 0.3s ease, color 0.3s ease;
}
/* Subtle grid texture overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
radial-gradient(ellipse 80% 60% at 20% 10%, rgba(0,201,184,0.07) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 80% 80%, rgba(0,150,140,0.04) 0%, transparent 55%);
pointer-events: none;
z-index: 0;
}
body > * { position: relative; z-index: 1; }
/* ── Animations ──────────────────────────────────────────────────── */
@keyframes fadeUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
@keyframes slideIn { from { opacity:0; transform:translateX(-10px); } to { opacity:1; transform:translateX(0); } }
@keyframes shimmer { 0% { background-position:-200% center; } 100% { background-position:200% center; } }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.6; } }
@keyframes spin { to { transform:rotate(360deg); } }
/* ── Nav ─────────────────────────────────────────────────────────── */
.nav {
height: var(--nav-h);
background: rgba(15,17,23,0.85);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 28px;
gap: 4px;
position: sticky;
top: 0;
z-index: 200;
box-shadow: 0 1px 0 var(--border), 0 4px 20px rgba(0,0,0,0.3);
}
[data-theme="light"] .nav {
background: rgba(240,242,248,0.88);
box-shadow: 0 1px 0 var(--border), 0 4px 20px rgba(0,0,0,0.08);
}
.nav-brand {
font-size: 17px;
font-weight: 700;
color: var(--text);
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
margin-right: 16px;
letter-spacing: -0.3px;
}
.nav-brand-icon {
width: 28px; height: 28px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border-radius: 7px;
display: flex; align-items: center; justify-content: center;
font-size: 14px;
box-shadow: 0 2px 8px var(--accent-glow);
}
.nav a:not(.nav-brand) {
color: var(--text2);
text-decoration: none;
font-size: 13.5px;
font-weight: 500;
padding: 6px 10px;
border-radius: var(--radius-sm);
transition: all 0.18s ease;
white-space: nowrap;
}
.nav a:not(.nav-brand):hover { color: var(--text); background: var(--surface2); }
.nav a.active { color: var(--accent2); background: var(--accent-glow); }
.nav-spacer { flex: 1; }
/* ── Nav dropdown ─────────────────────────────────────────────────── */
.nav-group { position: relative; }
.nav-group-btn {
display: flex; align-items: center; gap: 4px;
color: var(--text2);
font-size: 13.5px; font-weight: 500;
padding: 6px 10px;
border-radius: var(--radius-sm);
border: none; background: none; cursor: pointer;
transition: all 0.18s ease; white-space: nowrap;
}
.nav-group-btn:hover { color: var(--text); background: var(--surface2); }
.nav-group-btn.active { color: var(--accent2); background: var(--accent-glow); }
.nav-group-btn svg { width:10px; height:10px; transition: transform 0.18s ease; }
.nav-group:hover .nav-group-btn svg { transform: rotate(180deg); }
.nav-dropdown {
display: none;
position: absolute; top: calc(100% + 6px); left: 0;
background: var(--surface);
border: 1px solid var(--border2);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
min-width: 160px;
z-index: 200;
padding: 6px;
flex-direction: column; gap: 2px;
}
.nav-group:hover .nav-dropdown { display: flex; }
.nav-dropdown a {
display: block;
padding: 7px 12px;
border-radius: var(--radius-sm);
font-size: 13px; font-weight: 500;
color: var(--text2); text-decoration: none;
white-space: nowrap;
transition: all 0.15s ease;
}
.nav-dropdown a:hover { color: var(--text); background: var(--surface2); }
.nav-dropdown a.active { color: var(--accent2); background: var(--accent-glow); }
.nav-dropdown hr { border: none; border-top: 1px solid var(--border); margin: 4px 0; }
.nav-email {
font-size: 12px;
color: var(--text3);
padding: 0 8px;
}
.theme-btn {
width: 34px; height: 34px;
border-radius: 9px;
border: 1px solid var(--border2);
background: var(--surface2);
color: var(--text2);
cursor: pointer;
font-size: 15px;
display: flex; align-items: center; justify-content: center;
transition: all 0.18s ease;
}
.theme-btn:hover { color: var(--text); background: var(--bg3); transform: scale(1.05); }
/* ── Layout ──────────────────────────────────────────────────────── */
.container {
max-width: 1240px;
margin: 0 auto;
padding: 28px 24px 48px;
animation: fadeUp 0.4s ease-out both;
}
h1 {
font-size: 22px;
font-weight: 700;
color: var(--text);
letter-spacing: -0.4px;
}
/* ── Cards ───────────────────────────────────────────────────────── */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 22px;
margin-bottom: 16px;
box-shadow: var(--shadow-sm);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.card:hover {
box-shadow: var(--shadow-md);
border-color: var(--border2);
}
.card h2 {
font-size: 11px;
font-weight: 600;
color: var(--text3);
text-transform: uppercase;
letter-spacing: 0.8px;
margin-bottom: 10px;
}
.card .value {
font-size: 30px;
font-weight: 700;
letter-spacing: -0.8px;
line-height: 1.1;
}
/* value cards with colored top accent */
.value-card { position: relative; overflow: hidden; }
.value-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: var(--radius);
background: linear-gradient(135deg, var(--accent-glow), transparent 60%);
pointer-events: none;
}
.value-card::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent2), transparent);
border-radius: var(--radius) var(--radius) 0 0;
background-size: 200% auto;
animation: shimmer 3s ease-in-out infinite;
}
/* ── Colour utilities ────────────────────────────────────────────── */
.positive { color: var(--green); }
.negative { color: var(--red); }
/* ── Grid ────────────────────────────────────────────────────────── */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 16px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
/* ── Tables ──────────────────────────────────────────────────────── */
.table-wrap { overflow-x: auto; border-radius: var(--radius-sm); }
table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
th {
text-align: left;
padding: 9px 12px;
font-size: 11px;
font-weight: 600;
color: var(--text3);
text-transform: uppercase;
letter-spacing: 0.6px;
border-bottom: 1px solid var(--border2);
white-space: nowrap;
}
td {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
vertical-align: middle;
}
tbody tr { transition: background 0.12s ease; }
tbody tr:hover td { background: var(--surface2); }
tbody tr:last-child td { border-bottom: none; }
.cents { text-align: right; font-variant-numeric: tabular-nums; }
/* ── Buttons ─────────────────────────────────────────────────────── */
.btn {
display: inline-flex; align-items: center; gap: 5px;
padding: 8px 16px;
border-radius: var(--radius-sm);
font-size: 13.5px;
font-weight: 500;
border: none;
cursor: pointer;
text-decoration: none;
transition: all 0.18s ease;
white-space: nowrap;
line-height: 1;
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-primary {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%);
color: #fff;
box-shadow: 0 2px 10px var(--accent-glow);
}
.btn-primary:hover { box-shadow: 0 4px 18px var(--accent-glow); }
.btn-danger {
background: var(--red-dim);
color: var(--red);
border: 1px solid rgba(248,113,113,0.2);
}
.btn-danger:hover { background: rgba(248,113,113,0.25); }
.btn-outline {
background: transparent;
border: 1px solid var(--border2);
color: var(--text2);
}
.btn-outline:hover { background: var(--surface2); color: var(--text); border-color: var(--accent); }
.btn-sm { padding: 4px 10px; font-size: 12px; border-radius: 6px; }
/* ── Forms ───────────────────────────────────────────────────────── */
.form-group { margin-bottom: 14px; }
.form-group label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--text2);
margin-bottom: 5px;
letter-spacing: 0.3px;
}
.form-label {
display: block;
font-size: 11px;
font-weight: 700;
color: var(--text3);
margin-bottom: 5px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.form-group input,
.form-group select,
.form-group textarea,
.form-input {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border2);
border-radius: var(--radius-sm);
font-size: 13.5px;
background: var(--bg2);
color: var(--text);
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
outline: none;
box-sizing: border-box;
font-family: inherit;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus,
.form-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
background: var(--bg3);
}
.form-group input::placeholder,
.form-input::placeholder { color: var(--text3); opacity: 0.7; }
.form-input[type="file"] {
padding: 7px 10px;
cursor: pointer;
color: var(--text2);
}
.form-input[type="file"]::-webkit-file-upload-button {
background: var(--bg3);
border: 1px solid var(--border2);
border-radius: 4px;
color: var(--text2);
font-size: 12px;
padding: 4px 10px;
margin-right: 10px;
cursor: pointer;
font-family: inherit;
}
/* Global catch-all: any bare input/select/textarea gets dark theme.
.form-group and .form-input rules above still win for their elements
due to specificity; this only fixes uncovered stragglers. */
input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=submit]):not([type=button]):not([type=reset]):not([type=hidden]):not([type=color]),
select,
textarea {
background: var(--bg2);
color: var(--text);
border-color: var(--border2);
font-family: inherit;
}
input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=submit]):not([type=button]):not([type=reset]):not([type=hidden]):not([type=color]):focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
input::placeholder, textarea::placeholder { color: var(--text3); opacity: 0.7; }
select option { background: var(--bg2); color: var(--text); }
input[type="color"] { padding: 4px; height: 38px; cursor: pointer; }
.form-input option { background: var(--bg2); color: var(--text); }
textarea.form-input { resize: vertical; min-height: 72px; line-height: 1.5; }
/* Team / org avatars */
.team-avatar {
display: inline-flex; align-items: center; justify-content: center;
width: 32px; height: 32px;
border-radius: 8px;
font-size: 17px;
line-height: 1;
flex-shrink: 0;
background: var(--bg3);
border: 1px solid var(--border2);
user-select: none;
}
.team-avatar-sm {
width: 22px; height: 22px;
border-radius: 5px;
font-size: 12px;
}
.team-avatar-lg {
width: 48px; height: 48px;
border-radius: 12px;
font-size: 26px;
}
/* Emoji picker row */
.emoji-picker { display: flex; flex-wrap: wrap; gap: 6px; }
.emoji-opt {
width: 36px; height: 36px;
border-radius: 8px;
border: 2px solid transparent;
background: var(--bg3);
font-size: 18px;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: border-color 0.12s, background 0.12s;
}
.emoji-opt:hover { background: var(--surface2); }
.emoji-opt.selected { border-color: var(--accent); background: var(--accent-glow); }
/* ── Badges ──────────────────────────────────────────────────────── */
.badge {
display: inline-flex; align-items: center; gap: 5px;
padding: 3px 10px;
border-radius: 20px;
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.2px;
}
.category-dot {
width: 7px; height: 7px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
/* ── Empty states ────────────────────────────────────────────────── */
.empty-state {
text-align: center;
padding: 52px 24px;
color: var(--text3);
}
.empty-state h3 { font-size: 17px; color: var(--text2); margin-bottom: 8px; }
/* ── Misc utils ──────────────────────────────────────────────────── */
.flex { display: flex; gap: 8px; align-items: center; }
.flex-wrap { flex-wrap: wrap; }
.mb-16 { margin-bottom: 16px; }
.mb-8 { margin-bottom: 8px; }
.mt-16 { margin-top: 16px; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-muted { color: var(--text3); font-size: 12px; }
.error { color: var(--red); font-size: 13px; margin-bottom: 12px; }
.success { color: var(--green); font-size: 13px; margin-bottom: 12px; }
/* ── Scroll-reveal ───────────────────────────────────────────────── */
.animate-on-scroll {
opacity: 0; transform: translateY(18px);
transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}
.animate-on-scroll.visible { opacity: 1; transform: translateY(0); }
.animate-on-scroll:nth-child(2) { transition-delay: 0.07s; }
.animate-on-scroll:nth-child(3) { transition-delay: 0.14s; }
.animate-on-scroll:nth-child(4) { transition-delay: 0.21s; }
.animate-on-scroll:nth-child(5) { transition-delay: 0.28s; }
/* ── Hamburger / mobile drawer ────────────────────────────────────── */
.nav-hamburger {
display: none;
width: 36px; height: 36px;
border: none; background: none; cursor: pointer;
color: var(--text2);
flex-direction: column; justify-content: center; align-items: center; gap: 5px;
border-radius: var(--radius-sm);
transition: background 0.18s;
}
.nav-hamburger:hover { background: var(--surface2); }
.nav-hamburger span {
display: block; width: 20px; height: 2px;
background: currentColor; border-radius: 2px;
transition: transform 0.25s ease, opacity 0.25s ease;
}
.nav-hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-hamburger.open span:nth-child(2) { opacity: 0; }
.nav-hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.nav-drawer {
display: none;
position: fixed; inset: var(--nav-h) 0 0 0;
background: var(--bg);
z-index: 199;
overflow-y: auto;
padding: 12px 16px 32px;
flex-direction: column; gap: 4px;
animation: slideIn 0.2s ease-out both;
}
.nav-drawer.open { display: flex; }
.nav-drawer a, .nav-drawer-section-label {
display: block;
padding: 11px 14px;
border-radius: var(--radius-sm);
font-size: 15px; font-weight: 500;
color: var(--text2); text-decoration: none;
transition: all 0.15s;
}
.nav-drawer a:hover { color: var(--text); background: var(--surface2); }
.nav-drawer a.active { color: var(--accent2); background: var(--accent-glow); }
.nav-drawer-section-label {
font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--text3);
padding-top: 16px; padding-bottom: 4px;
}
.nav-drawer hr { border: none; border-top: 1px solid var(--border); margin: 8px 0; }
/* ── Responsive ──────────────────────────────────────────────────── */
@media (max-width: 900px) {
.grid-2 { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.nav-hamburger { display: flex; }
.nav > a:not(.nav-brand),
.nav > .nav-group,
.nav > .nav-spacer,
.nav-email { display: none; }
.container { padding: 16px 12px 32px; }
.grid { grid-template-columns: 1fr 1fr; }
.card { padding: 16px; }
}
</style>
</head>
<body>
<nav class="nav">
<a href="/dashboard" class="nav-brand">
<div class="nav-brand-icon"></div>
Personal
</a>
<a href="/dashboard" class="{{if eq .Route "dashboard"}}active{{end}}">Dashboard</a>
<a href="/transactions" class="{{if eq .Route "transactions"}}active{{end}}">Transactions</a>
<a href="/portfolio" class="{{if eq .Route "portfolio"}}active{{end}}">Portfolio</a>
<a href="/goals" class="{{if eq .Route "goals"}}active{{end}}">Goals</a>
<a href="/property" class="{{if eq .Route "property"}}active{{end}}">Property</a>
{{$analysisActive := or (eq .Route "reports") (eq .Route "projections") (eq .Route "networth") (eq .Route "simulator") (eq .Route "tax")}}
<div class="nav-group">
<button class="nav-group-btn {{if $analysisActive}}active{{end}}">
Analysis <svg viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
</button>
<div class="nav-dropdown">
<a href="/reports" class="{{if eq .Route "reports"}}active{{end}}">Reports</a>
<a href="/projections" class="{{if eq .Route "projections"}}active{{end}}">Projections</a>
<a href="/tax" class="{{if eq .Route "tax"}}active{{end}}">Tax</a>
<hr>
<a href="/networth" class="{{if eq .Route "networth"}}active{{end}}">Net Worth</a>
<a href="/simulator" class="{{if eq .Route "simulator"}}active{{end}}">What If</a>
</div>
</div>
<a href="/people" class="{{if eq .Route "people"}}active{{end}}">People</a>
{{$settingsActive := or (eq .Route "settings") (eq .Route "auto-import")}}
<div class="nav-group">
<button class="nav-group-btn {{if $settingsActive}}active{{end}}">
Settings <svg viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
</button>
<div class="nav-dropdown">
<a href="/settings?tab=accounts" class="{{if eq .Route "settings"}}active{{end}}">Accounts & Categories</a>
<a href="/import" class="{{if eq .Route "import"}}active{{end}}">Import CSV</a>
<a href="/auto-import" class="{{if eq .Route "auto-import"}}active{{end}}">Import Guide</a>
</div>
</div>
<div class="nav-spacer"></div>
<a href="/orgs" style="font-size:12px; color:var(--text3); padding:5px 9px; border:1px solid var(--border); border-radius:var(--radius-sm); text-decoration:none; transition:all 0.18s;" onmouseover="this.style.color='var(--text2)';this.style.borderColor='var(--border2)'" onmouseout="this.style.color='var(--text3)';this.style.borderColor='var(--border)'">🏢 Business</a>
<span class="nav-email">{{.Email}}</span>
<button class="theme-btn" id="theme-toggle" title="Toggle dark/light mode">🌙</button>
<button class="nav-hamburger" id="nav-hamburger" aria-label="Menu">
<span></span><span></span><span></span>
</button>
</nav>
<!-- Mobile drawer -->
<div class="nav-drawer" id="nav-drawer">
<a href="/dashboard" class="{{if eq .Route "dashboard"}}active{{end}}">Dashboard</a>
<a href="/transactions" class="{{if eq .Route "transactions"}}active{{end}}">Transactions</a>
<a href="/portfolio" class="{{if eq .Route "portfolio"}}active{{end}}">Portfolio</a>
<a href="/goals" class="{{if eq .Route "goals"}}active{{end}}">Goals</a>
<a href="/property" class="{{if eq .Route "property"}}active{{end}}">Property</a>
<a href="/people" class="{{if eq .Route "people"}}active{{end}}">People</a>
<hr>
<span class="nav-drawer-section-label">Analysis</span>
<a href="/reports" class="{{if eq .Route "reports"}}active{{end}}">Reports</a>
<a href="/projections" class="{{if eq .Route "projections"}}active{{end}}">Projections</a>
<a href="/tax" class="{{if eq .Route "tax"}}active{{end}}">Tax</a>
<a href="/networth" class="{{if eq .Route "networth"}}active{{end}}">Net Worth</a>
<a href="/simulator" class="{{if eq .Route "simulator"}}active{{end}}">What If</a>
<hr>
<span class="nav-drawer-section-label">Settings</span>
<a href="/settings?tab=accounts" class="{{if eq .Route "settings"}}active{{end}}">Accounts &amp; Categories</a>
<a href="/import" class="{{if eq .Route "import"}}active{{end}}">Import CSV</a>
<a href="/auto-import" class="{{if eq .Route "auto-import"}}active{{end}}">Import Guide</a>
<hr>
<a href="/orgs">🏢 Business</a>
<a href="/">← Hub</a>
</div>
<div class="container">
{{block "content" .}}{{end}}
</div>
<script>
/* ── Theme toggle ─────────────────────────────────────────────── */
const html = document.documentElement;
const btn = document.getElementById('theme-toggle');
function applyTheme(t) {
html.setAttribute('data-theme', t);
btn.textContent = t === 'dark' ? '☀️' : '🌙';
localStorage.setItem('theme', t);
}
const saved = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
applyTheme(saved);
btn.addEventListener('click', () =>
applyTheme(html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'));
/* ── Mobile hamburger ─────────────────────────────────────────── */
const hamburger = document.getElementById('nav-hamburger');
const drawer = document.getElementById('nav-drawer');
hamburger.addEventListener('click', () => {
const open = drawer.classList.toggle('open');
hamburger.classList.toggle('open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
drawer.querySelectorAll('a').forEach(a =>
a.addEventListener('click', () => {
drawer.classList.remove('open');
hamburger.classList.remove('open');
document.body.style.overflow = '';
})
);
/* ── Animated counter ─────────────────────────────────────────── */
function animateCounter(el) {
const target = parseFloat(el.getAttribute('data-target'));
const prefix = el.getAttribute('data-prefix') || '';
const duration = parseInt(el.getAttribute('data-duration')) || 900;
const start = performance.now();
function step(now) {
const t = Math.min((now - start) / duration, 1);
const e = 1 - Math.pow(1 - t, 3);
const v = target * e;
const abs = Math.abs(v);
const sign = v < 0 ? '' : (target > 0 ? '+' : '');
el.textContent = prefix + sign + (abs / 100).toLocaleString('pt-PT', {minimumFractionDigits:2, maximumFractionDigits:2});
if (t < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ── Scroll reveal + counter trigger ─────────────────────────── */
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
entry.target.classList.add('visible');
entry.target.querySelectorAll('.animate-counter').forEach(c => {
if (!c.dataset.counted) { c.dataset.counted = '1'; animateCounter(c); }
});
io.unobserve(entry.target);
});
}, { threshold: 0.08 });
document.querySelectorAll('.animate-on-scroll').forEach(el => io.observe(el));
/* ── Chart.js defaults for dark/light ─────────────────────────── */
function getThemeColor(v) {
const dark = html.getAttribute('data-theme') === 'dark';
return {
gridColor: dark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.06)',
textColor: dark ? '#5c6585' : '#9fa8c7',
}[v];
}
Chart.defaults.color = () => getThemeColor('textColor');
Chart.defaults.borderColor = () => getThemeColor('gridColor');
</script>
</body>
</html>