{"id":800,"date":"2026-08-18T20:07:40","date_gmt":"2026-08-18T20:07:40","guid":{"rendered":"https:\/\/goclimb.com\/?page_id=800"},"modified":"2026-08-26T20:46:15","modified_gmt":"2026-08-26T20:46:15","slug":"geldflussrechner","status":"publish","type":"page","link":"https:\/\/goclimb.com\/en\/geldflussrechner\/","title":{"rendered":"Geldflussrechner"},"content":{"rendered":"\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&#038;family=IBM+Plex+Mono:wght@400;500&#038;display=swap\" rel=\"stylesheet\">\n<style>\n.gf-tool{\n\n  --brand:#2672B5;\n  --brand-deep:#144B7D;\n  --brand-soft:#E4EFF8;\n\n  --ground:#F1F5F9;\n  --paper:#FFFFFF;\n  --ink:#123E63;\n  --ink-soft:#63788C;\n  --rule:#D9E3EC;\n\n  --green:#18A87A;\n  --coral:#F2704B;\n  --amber:#F2B33D;\n  --violet:#7A6BD6;\n  --sky:#3FA9E0;\n  --rose:#E2617F;\n}\n.gf-tool, .gf-tool *{box-sizing:border-box}\n.gf-tool{\n  background:var(--ground);\n  color:var(--ink);\n  font-family:\"Plus Jakarta Sans\",system-ui,sans-serif;\n  font-size:15px;\n  line-height:1.5;\n  position:relative;\n}\n.gf-tool::before{\n  content:\"\";position:absolute;inset:0;z-index:0;pointer-events:none;\n  background:radial-gradient(1100px 520px at 15% -10%, #E4EFF8 0%, rgba(228,239,248,0) 62%);\n}\n.gf-tool .wrap{position:relative;z-index:1;max-width:1400px;margin:0 auto;padding:130px 28px 80px}\n@media (max-width:1000px){.gf-tool .wrap{padding-top:100px}}\n\n.gf-tool header{display:flex;flex-wrap:wrap;align-items:flex-end;gap:20px 40px;margin-bottom:34px}\n.gf-tool h1{\n  font-weight:800;\n  font-size:clamp(30px,4.4vw,44px);\n  line-height:1.05;\n  letter-spacing:-.03em;\n  margin:0;\n}\n.gf-tool h1 em{font-style:normal;color:var(--brand)}\n.gf-tool .wordmark{font-weight:800;font-size:24px;letter-spacing:-.03em;color:var(--brand)}\n.gf-tool .lede{max-width:48ch;color:var(--ink-soft);font-size:14px;margin:0}\n\n.gf-tool .rule{height:1px;background:var(--rule);margin:0 0 26px}\n.gf-tool .warn{\n  display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:14px;\n  background:#FFF3E8;border:1px solid #F6C9A6;border-radius:12px;\n  padding:14px 18px;margin:0 0 22px;font-size:13.5px;line-height:1.5;color:#8A4A17;\n}\n.gf-tool .warn[hidden]{display:none}\n.gf-tool .warn b{color:#6E3910}\n.gf-tool .warn em{font-style:normal;font-family:\"IBM Plex Mono\",monospace;font-size:12.5px}\n.gf-tool .warn button{border-color:#E0A97C;color:#8A4A17;background:transparent;flex:none}\n.gf-tool .warn button:hover{background:#F9E2CE;border-color:#F9E2CE}\n\n.gf-tool .layout{display:grid;grid-template-columns:330px minmax(0,1fr);gap:34px;align-items:start}\n@media (max-width:1000px){.gf-tool .layout{grid-template-columns:1fr}}\n@media (min-width:1001px){.gf-tool .chartcol{position:sticky;top:100px}}\n\n.gf-tool .panel{background:var(--paper);border:1px solid var(--rule);border-radius:16px;padding:22px;\n  box-shadow:0 1px 2px rgba(18,62,99,.05),0 12px 28px rgba(18,62,99,.06)}\n.gf-tool .eyebrow{\n  font-size:11px;font-weight:700;letter-spacing:.09em;\n  text-transform:uppercase;color:var(--ink-soft);margin:0 0 12px;\n}\n.gf-tool .group+.group{margin-top:26px}\n\n.gf-tool .row{display:grid;grid-template-columns:11px 1fr 84px;align-items:center;gap:9px;padding:3px 0}\n.gf-tool .row+.row{border-top:1px solid rgba(205,214,209,.5)}\n.gf-tool .chip{width:9px;height:9px;border-radius:50%}\n.gf-tool .row label{font-size:13px;color:var(--ink);cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.gf-tool .row input{\n  font-family:\"IBM Plex Mono\",monospace;font-size:13px;text-align:right;\n  width:100%;padding:4px 6px;border:1px solid transparent;background:transparent;\n  color:var(--ink);border-radius:2px;\n}\n.gf-tool .row input:hover{border-color:var(--rule);background:#F7F9FD}\n.gf-tool .row input:focus{outline:none;border-color:var(--brand);background:var(--brand-soft)}\n.gf-tool .row input::-webkit-outer-spin-button,.gf-tool .row input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}\n.gf-tool .row input[type=number]{-moz-appearance:textfield}\n\n.gf-tool textarea{\n  width:100%;font-family:\"IBM Plex Mono\",monospace;font-size:12px;line-height:1.45;\n  padding:8px;border:1px dashed var(--rule);background:transparent;color:var(--ink);\n  border-radius:2px;resize:vertical;\n}\n.gf-tool textarea:focus{outline:none;border-color:var(--brand);border-style:solid;background:var(--brand-soft)}\n.gf-tool .importbox{border-top:1px solid var(--rule);padding-top:16px}\n.gf-tool .importbox summary{\n  font-size:11px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;\n  color:var(--ink-soft);cursor:pointer;list-style:none;display:flex;align-items:center;gap:7px;\n}\n.gf-tool .importbox summary::-webkit-details-marker{display:none}\n.gf-tool .importbox summary::before{content:\"+\";font-size:13px;line-height:1}\n.gf-tool .importbox[open] summary::before{content:\"\\2013\"}\n.gf-tool .importbox summary:hover{color:var(--ink)}\n.gf-tool .importhelp{font-size:11.5px;color:var(--ink-soft);margin:10px 0 8px;line-height:1.45}\n.gf-tool .importhelp em{font-style:normal;font-family:\"IBM Plex Mono\",monospace;color:var(--ink)}\n.gf-tool .importnote{font-size:11px;color:var(--ink-soft);margin:6px 0 0}\n.gf-tool .importnote.ok{color:var(--green)}\n.gf-tool .importnote.warn{color:var(--coral)}\n.gf-tool .totals{margin-top:20px;border-top:2px solid var(--brand);padding-top:12px}\n.gf-tool .tline{display:flex;justify-content:space-between;align-items:baseline;padding:3px 0;font-size:13px}\n.gf-tool .tline b{font-family:\"IBM Plex Mono\",monospace;font-weight:500}\n.gf-tool .tline.major{font-size:15px;padding-top:8px}\n.gf-tool .tline.major b{font-size:17px}\n\n.gf-tool .stage{background:var(--paper);border:1px solid var(--rule);border-radius:16px;padding:10px 10px 6px;position:relative;\n  box-shadow:0 1px 2px rgba(18,62,99,.05),0 12px 28px rgba(18,62,99,.06)}\n.gf-tool svg{display:block;width:100%;height:auto}\n\n.gf-tool .actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px;align-items:flex-start}\n.gf-tool .menu{position:relative}\n.gf-tool .caret{\n  display:inline-block;width:0;height:0;margin-left:9px;vertical-align:middle;\n  border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid currentColor;\n}\n.gf-tool .menupanel{\n  position:absolute;top:calc(100% + 6px);left:0;min-width:290px;z-index:20;\n  background:var(--paper);border:1px solid var(--rule);border-radius:14px;padding:6px;\n  box-shadow:0 8px 30px rgba(18,62,99,.16);\n  opacity:0;visibility:hidden;transform:translateY(-4px);\n  transition:opacity .14s ease,transform .14s ease,visibility .14s;\n}\n.gf-tool .menu:hover .menupanel,.gf-tool .menu:focus-within .menupanel,.gf-tool .menu.open .menupanel{\n  opacity:1;visibility:visible;transform:translateY(0);\n}\n.gf-tool .menu::after{content:\"\";position:absolute;left:0;right:0;top:100%;height:10px}\n.gf-tool .menuitem{\n  display:block;width:100%;text-align:left;background:transparent;border:0;\n  padding:10px 12px;border-radius:9px;cursor:pointer;color:var(--ink);\n}\n.gf-tool .menuitem:hover{background:var(--brand-soft);filter:none}\n.gf-tool .menuitem:focus-visible{outline:2px solid var(--brand);outline-offset:-2px}\n.gf-tool .mi-title{display:block;font-size:13.5px;font-weight:600}\n.gf-tool .mi-sub{display:block;font-size:11.5px;font-weight:400;color:var(--ink-soft);margin-top:2px}\n.gf-tool button{\n  font-family:\"Plus Jakarta Sans\",sans-serif;font-size:13.5px;font-weight:600;\n  padding:11px 20px;border:1px solid var(--brand);background:var(--brand);color:#fff;\n  border-radius:999px;cursor:pointer;transition:filter .15s,background .15s,color .15s;\n}\n.gf-tool button:hover{filter:brightness(1.08)}\n.gf-tool button:focus-visible{outline:2px solid var(--brand);outline-offset:3px}\n.gf-tool button.ghost{border-color:var(--rule);background:transparent;color:var(--ink)}\n.gf-tool button.ghost:hover{background:var(--brand-soft);border-color:var(--brand-soft);filter:none}\n.gf-tool .filebtn{\n  font-size:13.5px;font-weight:600;padding:11px 20px;border:1px solid var(--rule);\n  color:var(--ink);border-radius:999px;cursor:pointer;transition:background .15s;\n}\n.gf-tool .filebtn:hover{background:var(--brand-soft);border-color:var(--brand-soft)}\n.gf-tool .filebtn input{display:none}\n.gf-tool .filebtn:focus-within{outline:2px solid var(--brand);outline-offset:3px}\n.gf-tool .hint{font-size:12px;color:var(--ink-soft);margin:10px 0 0}\n.gf-tool .linkbox[hidden]{display:none}\n.gf-tool .linkbox{margin-top:14px;padding:14px 16px;background:var(--brand-soft);border-radius:12px}\n.gf-tool .linkbox input{\n  width:100%;font-family:\"IBM Plex Mono\",monospace;font-size:11.5px;\n  padding:9px 11px;border:1px solid var(--rule);border-radius:8px;background:#fff;color:var(--ink);\n}\n.gf-tool .linkbox input:focus{outline:2px solid var(--brand);outline-offset:1px}\n.gf-tool .linkbox .hint{margin-top:8px}\n\n.gf-tool .ribbon{opacity:0;animation:gf-grow .7s cubic-bezier(.22,.61,.36,1) forwards}\n.gf-tool .nodebar,.gf-tool .lbl{opacity:0;animation:gf-fade .5s ease forwards}\n@keyframes gf-grow{from{opacity:0;transform:scaleX(.82);transform-origin:center}to{opacity:.78;transform:scaleX(1)}}\n@keyframes gf-fade{to{opacity:1}}\n@media (prefers-reduced-motion:reduce){\n  .gf-tool .ribbon{animation:none;opacity:.78}\n  .gf-tool .nodebar,.gf-tool .lbl{animation:none;opacity:1}\n}\n<\/style>\n<div class=\"gf-tool\">\n<div class=\"wrap\">\n\n<header>\n  <div>\n    <h1>Dein <em>Geldfluss<\/em><\/h1>\n  <\/div>\n  <p class=\"lede\">Trag die Betr\u00e4ge eines Monats ein \u2014 das Diagramm zeichnet sich beim Tippen neu. Am Ende als Bild, PDF oder Vektordatei herunterladen.<\/p>\n<\/header>\n<div id=\"frameWarn\" class=\"warn\" hidden>\n  <div>\n    <b>Du siehst eine eingebettete Vorschau.<\/b>\n    Speichern und Links sind hier vom Browser gesperrt \u2013 daher die Meldung \u201eDieser Inhalt ist blockiert&#8220;.\n    Lade die Datei <em>Geldfluss-Sankey.html<\/em> herunter und \u00f6ffne sie per Doppelklick, dann funktioniert alles.\n  <\/div>\n  <button id=\"popOut\" class=\"ghost\">In eigenem Tab \u00f6ffnen<\/button>\n<\/div>\n<div class=\"rule\"><\/div>\n\n<div class=\"layout\">\n\n  <div class=\"panel\">\n    <div class=\"group\">\n      <p class=\"eyebrow\">Einnahmen<\/p>\n      <div id=\"listIn\"><\/div>\n    <\/div>\n    <div class=\"group\">\n      <p class=\"eyebrow\">Feste Ausgaben<\/p>\n      <div id=\"listFix\"><\/div>\n    <\/div>\n    <div class=\"group\">\n      <p class=\"eyebrow\">Variable Ausgaben<\/p>\n      <div id=\"listVar\"><\/div>\n    <\/div>\n    <div class=\"group\">\n      <p class=\"eyebrow\">Sparen<\/p>\n      <div id=\"listSpar\"><\/div>\n    <\/div>\n\n    <details class=\"group importbox\">\n      <summary>Werte aus der Excel-Vorlage \u00fcbernehmen<\/summary>\n      <p class=\"importhelp\">Nur n\u00f6tig, wenn du den Haushaltsplan als Excel f\u00fchrst. In der Datei\n        <em>Haushaltsplan.xlsx<\/em> das Blatt <em>Sankey-Export<\/em> \u00f6ffnen, den Bereich A5:B36 kopieren\n        und hier einf\u00fcgen. Ohne Excel einfach die Felder oben ausf\u00fcllen.<\/p>\n      <textarea id=\"paste\" rows=\"3\" placeholder=\"Kopierte Zellen hier einf\u00fcgen\"><\/textarea>\n      <p class=\"importnote\" id=\"importNote\">Die Betr\u00e4ge oben werden dabei \u00fcberschrieben.<\/p>\n    <\/details>\n\n    <div class=\"totals\">\n      <div class=\"tline\"><span>Einnahmen<\/span><b id=\"tIn\">\u2013<\/b><\/div>\n      <div class=\"tline\"><span>Ausgaben<\/span><b id=\"tOut\">\u2013<\/b><\/div>\n      <div class=\"tline\"><span>Sparen<\/span><b id=\"tSpar\">\u2013<\/b><\/div>\n      <div class=\"tline major\"><span id=\"restLbl\">\u00dcberschuss<\/span><b id=\"tRest\">\u2013<\/b><\/div>\n      <div class=\"tline\"><span>Sparquote<\/span><b id=\"tQuote\">\u2013<\/b><\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"chartcol\">\n    <div class=\"stage\"><div id=\"chart\"><\/div><\/div>\n    <div class=\"actions\">\n      <div class=\"menu\" id=\"dlMenu\">\n        <button id=\"btnDl\" aria-haspopup=\"true\" aria-expanded=\"false\">Herunterladen<span class=\"caret\"><\/span><\/button>\n        <div class=\"menupanel\" role=\"menu\">\n          <button class=\"menuitem\" id=\"dlPng\" role=\"menuitem\">\n            <span class=\"mi-title\">Bild (PNG)<\/span>\n            <span class=\"mi-sub\">F\u00fcr Pr\u00e4sentation, Social Media, Blog<\/span>\n          <\/button>\n          <button class=\"menuitem\" id=\"dlPdf\" role=\"menuitem\">\n            <span class=\"mi-title\">PDF<\/span>\n            <span class=\"mi-sub\">Eine Seite, zum Drucken oder Verschicken<\/span>\n          <\/button>\n          <button class=\"menuitem\" id=\"dlSvg\" role=\"menuitem\">\n            <span class=\"mi-title\">Vektordatei (SVG)<\/span>\n            <span class=\"mi-sub\">Skalierbar, in Figma oder Illustrator \u00e4nderbar<\/span>\n          <\/button>\n        <\/div>\n      <\/div>\n      <button id=\"btnLink\" class=\"ghost\">Link zur Berechnung<\/button>\n      <button id=\"btnClear\" class=\"ghost\">Alle Betr\u00e4ge leeren<\/button>\n    <\/div>\n    <div id=\"linkBox\" class=\"linkbox\" hidden>\n      <input id=\"linkField\" type=\"text\" readonly onfocus=\"this.select()\">\n      <p class=\"hint\" id=\"linkNote\"><\/p>\n    <\/div>\n    <p class=\"hint\">Beim Herunterladen mit der Maus \u00fcber den Knopf fahren, dann erscheint die Formatauswahl. Kategorien ohne Betrag werden im Diagramm weggelassen; die Reihenfolge rechts richtet sich nach der H\u00f6he.<\/p>\n  <\/div>\n\n<\/div>\n<\/div>\n<\/div>\n\n<script>\nconst LOGO_DATA_URI = \"data:image\/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4gPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCAyNTYgNjAiIGZpbGw9Im5vbmUiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY0LjA3MSA0LjcyNjQ1VjU4Ljg2NTJIMTc0Ljc3VjUzLjgzOThDMTc3Ljc2OSA1Ny41NjgzIDE4Mi4yMjcgNjAgMTg4LjA2MyA2MEMxOTkuNDExIDYwIDIwOC4yNDYgNTAuNzU5NyAyMDguMjQ2IDM4LjM1ODJDMjA4LjI0NiAyNS44NzU2IDE5OS40MTEgMTYuNzE2NCAxODguMDYzIDE2LjcxNjRDMTgyLjIyNyAxNi43MTY0IDE3Ny43NjkgMTkuMDY3IDE3NC43NyAyMi43OTU1VjBMMTY0LjA3MSA0LjcyNjQ1Wk0xOTcuNzA5IDM4LjI3NzFDMTk3LjcwOSA0NS40MSAxOTIuOTI2IDUwLjQzNTUgMTg2LjE5OSA1MC40MzU1QzE3OS43MTQgNTAuNDM1NSAxNzQuNTI3IDQ1LjQxIDE3NC41MjcgMzguMjc3MUMxNzQuNTI3IDMxLjIyNTMgMTc5LjcxNCAyNi4yODA5IDE4Ni4xOTkgMjYuMjgwOUMxOTIuOTI2IDI2LjI4MDkgMTk3LjcwOSAzMS4yMjUzIDE5Ny43MDkgMzguMjc3MVoiIGZpbGw9IiMyNjcyQjUiPjwvcGF0aD48cGF0aCBkPSJNMCAzMC40MTUzQzAgNDcuMTkzOCAxMi43MjU3IDYwLjAwMDYgMjkuODI4NCA2MC4wMDA2QzQ0LjgyMzcgNjAuMDAwNiA1NC4zODgzIDUwLjg0MTMgNTYuOTgyIDQxLjc2MzFMNTcuOTU5NyAzOC4yMTU4TDQxLjc4NyA0NS4zNjAxQzM4LjgyMzggNDcuODY2NiAzNC42NzE0IDQ5LjU0NDQgMjkuNjY2MyA0OS41NDQ0QzE4LjQ4MDcgNDkuNTQ0NCAxMS4zNDc4IDQwLjU0NzMgMTEuMzQ3OCAzMC4zMzQzQzExLjM0NzggMjAuMTIxMyAxOC40ODA3IDExLjQ0ODMgMjkuNjY2MyAxMS40NDgzQzM4LjI1ODIgMTEuNDQ4MyA0NC4zMzc0IDE2LjU1NDggNDYuMjAxNiAyMi4wNjY2TDU3LjA2MzEgMTkuMzkxOEM1NC40NjkzIDEwLjM5NDYgNDQuOTA0OCAwLjk5MjE4OCAyOS41ODUzIDAuOTkyMTg4QzEyLjcyNTcgMC45OTIxODggMCAxMy43MTc5IDAgMzAuNDE1M1oiIGZpbGw9IiMyNjcyQjUiPjwvcGF0aD48cGF0aCBkPSJNNjAuNDMyOSA0Ljg4NTI0VjMzLjk0Nkw3MS4xMzIyIDI5LjIxOTVWMC4xNzk2ODhINzEuMDg0OUw2MC40MzI5IDQuODg1MjRaIiBmaWxsPSIjMjY3MkI1Ij48L3BhdGg+PHBhdGggZD0iTTYwLjQzMjkgNTguODY1OVYzNy4xMjM1TDcxLjEzMjIgMzIuMzk3VjU4Ljg2NTlINjAuNDMyOVoiIGZpbGw9IiMyNjcyQjUiPjwvcGF0aD48cGF0aCBkPSJNNzYuNjUgMTcuOTExN0wxMDQuMTg2IDUuNzQ3NTdMMTA0LjE4NyAxNC42MjcxTDc2LjY1MDcgMjYuNzkxM0w3Ni42NSAxNy45MTE3WiIgZmlsbD0iIzI2NzJCNSI+PC9wYXRoPjxwYXRoIGQ9Ik03Ni43OTgyIDI5Ljg5MzFWNTguODY0OEg4Ny41Nzg2VjI1LjEzMDlMNzYuNzk4MiAyOS44OTMxWiIgZmlsbD0iIzI2NzJCNSI+PC9wYXRoPjxwYXRoIGQ9Ik05My40ODIzIDIyLjUyMzNWNTguODY1N0gxMDQuMTgyVjM4LjI3NzZDMTA0LjE4MiAzMS4wNjM2IDEwNy4yNjIgMjYuNjg2NiAxMTMuMDE3IDI2LjY4NjZDMTE4LjUyOCAyNi42ODY2IDEyMC43MTcgMzAuNjU4MyAxMjAuNzE3IDM2LjczNzVWNTguODY1N0gxMzEuNDk3VjM3Ljk1MzNDMTMxLjQ5NyAzMC45ODI2IDEzNC40MTUgMjYuNjg2NiAxNDAuMTcgMjYuNjg2NkMxNDUuNzYzIDI2LjY4NjYgMTQ4LjAzMyAzMC41NzczIDE0OC4wMzMgMzYuNzM3NVY1OC44NjU3SDE1OC43MzJWMzQuNDY4QzE1OC43MzIgMjMuMDM5MSAxNTIuMzI5IDE2LjcxNjggMTQyLjUyMSAxNi43MTY4QzEzNS45NTUgMTYuNzE2OCAxMzEuMzM1IDE5LjQ3MjcgMTI4LjY2IDIyLjg3N0MxMjYuMTQ4IDE4Ljk4NjQgMTIyLjA5NSAxNi43MTY4IDExNi4zNCAxNi43MTY4QzExMC45MDkgMTYuNzE2OCAxMDYuODU2IDE5LjIyOTUgMTA0LjE4MiAyMi43MTQ5VjE3Ljg1MTZWMTcuNzc5TDkzLjQ4MjMgMjIuNTIzM1oiIGZpbGw9IiMyNjcyQjUiPjwvcGF0aD48cmVjdCB4PSIyMDguMjQ2IiB3aWR0aD0iNDUuODU4NyIgaGVpZ2h0PSIyNCIgcng9IjMuMTE3NTciIGZpbGw9IiMyNjcyQjUiPjwvcmVjdD48cGF0aCBkPSJNMjEyLjI4NSAxOUwyMTcuMzkzIDUuMDMzMjhIMjIwLjQ0NkwyMjUuNTM0IDE5SDIyMi42ODFMMjE4LjkxIDguMTA1OTZMMjE1LjExOSAxOUgyMTIuMjg1Wk0yMTQuNTQgMTUuODI3NkwyMTUuMjU4IDEzLjczMjZIMjIyLjM2MUwyMjMuMDYgMTUuODI3NkgyMTQuNTRaTTIyNy4zNjkgMTlWNS4wMzMyOEgyMzIuNTk3QzIzMy43MjcgNS4wMzMyOCAyMzQuNjY1IDUuMjI2MTUgMjM1LjQxIDUuNjExOUMyMzYuMTU1IDUuOTk3NjUgMjM2LjcwNyA2LjUxNjQxIDIzNy4wNjYgNy4xNjgxOUMyMzcuNDI1IDcuODE5OTcgMjM3LjYwNSA4LjU2NDg2IDIzNy42MDUgOS40MDI4N0MyMzcuNjA1IDEwLjE3NDQgMjM3LjQzMiAxMC44ODYgMjM3LjA4NiAxMS41Mzc4QzIzNi43NCAxMi4xODk2IDIzNi4xOTUgMTIuNzIxNiAyMzUuNDUgMTMuMTM0QzIzNC43MDUgMTMuNTMzIDIzMy43NTQgMTMuNzMyNiAyMzIuNTk3IDEzLjczMjZIMjMwLjA2M1YxOUgyMjcuMzY5Wk0yMzAuMDYzIDExLjU1NzdIMjMyLjQzN0MyMzMuMzAyIDExLjU1NzcgMjMzLjkyIDExLjM2NDkgMjM0LjI5MyAxMC45NzkxQzIzNC42NzggMTAuNTgwMSAyMzQuODcxIDEwLjA1NDYgMjM0Ljg3MSA5LjQwMjg3QzIzNC44NzEgOC43MTExOCAyMzQuNjc4IDguMTc5MTIgMjM0LjI5MyA3LjgwNjY3QzIzMy45MiA3LjQyMDkyIDIzMy4zMDIgNy4yMjgwNSAyMzIuNDM3IDcuMjI4MDVIMjMwLjA2M1YxMS41NTc3Wk0yMzkuNjI1IDE5VjUuMDMzMjhIMjQ0Ljg1M0MyNDUuOTgzIDUuMDMzMjggMjQ2LjkyMSA1LjIyNjE1IDI0Ny42NjYgNS42MTE5QzI0OC40MTEgNS45OTc2NSAyNDguOTYzIDYuNTE2NDEgMjQ5LjMyMiA3LjE2ODE5QzI0OS42ODEgNy44MTk5NyAyNDkuODYxIDguNTY0ODYgMjQ5Ljg2MSA5LjQwMjg3QzI0OS44NjEgMTAuMTc0NCAyNDkuNjg4IDEwLjg4NiAyNDkuMzQyIDExLjUzNzhDMjQ4Ljk5NiAxMi4xODk2IDI0OC40NTEgMTIuNzIxNiAyNDcuNzA2IDEzLjEzNEMyNDYuOTYxIDEzLjUzMyAyNDYuMDEgMTMuNzMyNiAyNDQuODUzIDEzLjczMjZIMjQyLjMxOVYxOUgyMzkuNjI1Wk0yNDIuMzE5IDExLjU1NzdIMjQ0LjY5M0MyNDUuNTU4IDExLjU1NzcgMjQ2LjE3NiAxMS4zNjQ5IDI0Ni41NDkgMTAuOTc5MUMyNDYuOTM0IDEwLjU4MDEgMjQ3LjEyNyAxMC4wNTQ2IDI0Ny4xMjcgOS40MDI4N0MyNDcuMTI3IDguNzExMTggMjQ2LjkzNCA4LjE3OTEyIDI0Ni41NDkgNy44MDY2N0MyNDYuMTc2IDcuNDIwOTIgMjQ1LjU1OCA3LjIyODA1IDI0NC42OTMgNy4yMjgwNUgyNDIuMzE5VjExLjU1NzdaIiBmaWxsPSJ3aGl0ZSI+PC9wYXRoPjwvc3ZnPiA=\";\n\nconst C = {\n  brand:\"#2672B5\", green:\"#18A87A\", coral:\"#F2704B\", clay:\"#F2704B\",\n  ink:\"#123E63\", inkSoft:\"#63788C\", paper:\"#FFFFFF\", rule:\"#D9E3EC\", moss:\"#18A87A\"\n};\n\/* Einnahmen bleiben in der Markenfamilie, damit der Zufluss als \u201eClimb-Blau\" lesbar ist. *\/\nconst BLUES = [\"#2672B5\",\"#3E8AC9\",\"#5CA1D8\",\"#144B7D\",\"#7FB8E4\",\"#1E6099\",\"#A2CDEC\",\"#0F3A61\",\"#C3E0F4\",\"#0A2942\"];\n\/* Feste Kosten: warm, weil unbeweglich. Variable Kosten: gemischt, aber ged\u00e4mpfter. *\/\nconst WARM  = [\"#F2704B\",\"#E8562F\",\"#F2B33D\",\"#D9531F\",\"#F58E6E\",\"#C9902A\"];\nconst COOL  = [\"#18A87A\",\"#3FA9E0\",\"#7A6BD6\",\"#E2617F\",\"#0E8A63\",\"#2E8FC4\",\"#C58E2A\",\"#9A8DE4\",\n               \"#3FBF95\",\"#EE8AA1\",\"#66B9E8\",\"#B87FD6\",\"#8FA0BC\",\"#E29A5B\",\"#4FB7D6\",\"#D67FA8\"];\n\n\/* ----------------------------------------------------------------\n   BRANDING\n   F\u00fcr die Produktivversion hier das echte Logo hinterlegen:\n   BRAND.logo = \"data:image\/svg+xml;base64,\u2026\"  (oder ein Pfad zur Datei)\n   Solange nichts hinterlegt ist, wird BRAND.text als Wortmarke gesetzt.\n   ---------------------------------------------------------------- *\/\nconst BRAND = {\n  text: \"Climb\",\n  claim: \"goclimb.com\",\n  \/\/ Climb-Logo als data:-URI eingebettet. Dadurch braucht die Seite kein Netz und\n  \/\/ das Logo landet zuverl\u00e4ssig in PNG und SVG. Seitenverh\u00e4ltnis 256:60.\n  logo: LOGO_DATA_URI,\n  logoW: 128,\n  logoH: 30\n};\n\nconst DATA = {\n  ein: [\n    [\"Gehalt \/ Lohn\", 2400],\n    [\"Gehalt Partner\/in\", 1600],\n    [\"Selbst\u00e4ndigkeit \/ Gewerbe\", 0],\n    [\"Rente \/ Pension\", 0],\n    [\"Kindergeld\", 250],\n    [\"Miet- \\u0026 Kapitalertr\u00e4ge\", 0],\n    [\"Sozialleistungen\", 0],\n    [\"R\u00fcckerstattung\", 0],\n    [\"Verkauf\", 0],\n    [\"Sonstige Einnahme\", 0]\n  ],\n  fix: [\n    [\"Miete \/ Wohnen\", 1150],\n    [\"Nebenkosten \\u0026 Energie\", 210],\n    [\"Internet, Telefon \\u0026 Rundfunk\", 65],\n    [\"Versicherungen\", 180],\n    [\"Kredite \\u0026 Ratenzahlungen\", 200],\n    [\"Abos \\u0026 Mitgliedschaften\", 45]\n  ],\n  vari: [\n    [\"Lebensmittel\", 620],\n    [\"Restaurant \\u0026 Ausgehen\", 150],\n    [\"Auto \\u0026 Kraftstoff\", 220],\n    [\"\u00d6PNV \\u0026 Bahn\", 60],\n    [\"Gesundheit \\u0026 Apotheke\", 50],\n    [\"Drogerie \\u0026 K\u00f6rperpflege\", 60],\n    [\"Kleidung \\u0026 Schuhe\", 90],\n    [\"Kinder \\u0026 Betreuung\", 180],\n    [\"Haustiere\", 0],\n    [\"Freizeit \\u0026 Hobby\", 120],\n    [\"Urlaub \\u0026 Reisen\", 150],\n    [\"Bildung \\u0026 Weiterbildung\", 0],\n    [\"Haushalt \\u0026 Anschaffungen\", 80],\n    [\"Geschenke \\u0026 Spenden\", 40],\n    [\"Sonstiges\", 50]\n  ],\n  spar: [\n    [\"Sparen \\u0026 Vorsorge\", 300]\n  ]\n};\n\nconst eur = n => new Intl.NumberFormat(\"de-DE\",{style:\"currency\",currency:\"EUR\",maximumFractionDigits:0}).format(Math.round(n));\nconst esc = s => s.replace(\/\\u0026\/g,\"&amp;\").replace(\/<\/g,\"&lt;\").replace(\/>\/g,\"&gt;\");\n\nfunction colorFor(kind, i){\n  if(kind===\"ein\")  return BLUES[i % BLUES.length];\n  if(kind===\"fix\")  return WARM[i % WARM.length];\n  if(kind===\"spar\") return C.green;\n  return COOL[i % COOL.length];\n}\n\n\/* ---------- Eingabeliste ---------- *\/\nfunction buildList(elId, kind){\n  const el = document.getElementById(elId);\n  el.innerHTML = DATA[kind].map((r,i)=>`\n    <div class=\"row\">\n      <span class=\"chip\" style=\"background:${colorFor(kind,i)}\"><\/span>\n      <label for=\"${kind}${i}\" title=\"${esc(r[0])}\">${esc(r[0])}<\/label>\n      <input id=\"${kind}${i}\" type=\"number\" min=\"0\" step=\"10\" value=\"${r[1]||\"\"}\"\n             placeholder=\"0\" data-kind=\"${kind}\" data-i=\"${i}\" inputmode=\"decimal\">\n    <\/div>`).join(\"\");\n}\n\n\/* ---------- Sankey-Geometrie ---------- *\/\nconst W = 1240, NODE = 15, PAD = 34;\nconst X0 = 258, X1 = 640, X2 = 946;\n\nfunction ribbonPath(xa, ya, xb, yb, h){\n  const mx = (xa + xb) \/ 2;\n  return `M${xa},${ya} C${mx},${ya} ${mx},${yb} ${xb},${yb} `\n       + `L${xb},${yb+h} C${mx},${yb+h} ${mx},${ya+h} ${xa},${ya+h} Z`;\n}\n\nfunction buildSvg(){\n  const inFlows = [];\n  DATA.ein.forEach((r,i)=>{ if(r[1]>0) inFlows.push({n:r[0], v:r[1], c:colorFor(\"ein\",i)}); });\n\n  let outFlows = [];\n  DATA.fix.forEach((r,i)=>{ if(r[1]>0) outFlows.push({n:r[0], v:r[1], c:colorFor(\"fix\",i)}); });\n  DATA.vari.forEach((r,i)=>{ if(r[1]>0) outFlows.push({n:r[0], v:r[1], c:colorFor(\"vari\",i)}); });\n  DATA.spar.forEach((r,i)=>{ if(r[1]>0) outFlows.push({n:r[0], v:r[1], c:colorFor(\"spar\",i)}); });\n\n  const tIn  = inFlows.reduce((a,b)=>a+b.v,0);\n  const tOut = outFlows.reduce((a,b)=>a+b.v,0);\n  const rest = tIn - tOut;\n  if(rest > 0) outFlows.push({n:\"\u00dcberschuss\", v:rest, c:\"#5FC2A3\", rest:true});\n  if(rest < 0) inFlows.push({n:\"Fehlbetrag\", v:-rest, c:C.clay, rest:true});\n\n  outFlows.sort((a,b)=> b.v - a.v);\n\n  if(!inFlows.length || !outFlows.length){\n    return {svg:`<svg viewBox=\"0 0 ${W} 300\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\n      <text x=\"${W\/2}\" y=\"150\" text-anchor=\"middle\" fill=\"${C.inkSoft}\"\n      font-family=\"Plus Jakarta Sans, sans-serif\" font-size=\"15\" font-weight=\"500\">Trag links Betr\u00e4ge ein, dann erscheint der Geldfluss.<\/text><\/svg>`,\n      tIn, tOut, rest};\n  }\n\n  const total = Math.max(tIn, tOut);\n  const gapL = 20, gapR = outFlows.length > 16 ? 16 : 13;\n  const spanL = 560 + gapL * (inFlows.length - 1);\n  const spanR = 560 + gapR * (outFlows.length - 1);\n  const H = Math.max(spanL, spanR) + PAD * 2 + 88;\n  const sc = 560 \/ total;\n\n  const place = (arr, gap, span) => {\n    let y = (H - 48 - span) \/ 2;\n    return arr.map(f => { const o = {...f, y, h: f.v * sc}; y += o.h + gap; return o; });\n  };\n  const L = place(inFlows, gapL, spanL);\n  const R = place(outFlows, gapR, spanR);\n  const midTop = (H - 48 - 560) \/ 2;\n\n  let ribbons = \"\", nodes = \"\", labels = \"\";\n  let cur = midTop, k = 0;\n  L.forEach(f=>{\n    ribbons += `<path class=\"ribbon\" style=\"animation-delay:${k*22}ms\" d=\"${ribbonPath(X0+NODE, f.y, X1, cur, f.h)}\" fill=\"${f.c}\"\/>`;\n    cur += f.h; k++;\n  });\n  cur = midTop;\n  R.forEach(f=>{\n    ribbons += `<path class=\"ribbon\" style=\"animation-delay:${k*22}ms\" d=\"${ribbonPath(X1+NODE, cur, X2, f.y, f.h)}\" fill=\"${f.c}\"\/>`;\n    cur += f.h; k++;\n  });\n\n  const d = 260;\n  L.forEach(f=>{\n    nodes += `<rect class=\"nodebar\" style=\"animation-delay:${d}ms\" x=\"${X0}\" y=\"${f.y}\" width=\"${NODE}\" height=\"${Math.max(f.h,2)}\" rx=\"2\" fill=\"${f.c}\"\/>`;\n    const cy = f.y + f.h\/2;\n    labels += `<text class=\"lbl\" style=\"animation-delay:${d}ms\" x=\"${X0-14}\" y=\"${cy-1}\" text-anchor=\"end\"\n      font-family=\"Plus Jakarta Sans, sans-serif\" font-size=\"13\" font-weight=\"${f.rest?600:400}\" fill=\"${f.rest?C.clay:C.ink}\">${esc(f.n)}<\/text>`;\n    labels += `<text class=\"lbl\" style=\"animation-delay:${d}ms\" x=\"${X0-14}\" y=\"${cy+14}\" text-anchor=\"end\"\n      font-family=\"IBM Plex Mono, monospace\" font-size=\"12\" fill=\"${C.inkSoft}\">${eur(f.v)}<\/text>`;\n  });\n\n  nodes += `<rect class=\"nodebar\" style=\"animation-delay:${d}ms\" x=\"${X1}\" y=\"${midTop}\" width=\"${NODE}\" height=\"560\" rx=\"3\" fill=\"${C.brand}\"\/>`;\n  labels += `<rect class=\"lbl\" style=\"animation-delay:${d}ms\" x=\"${X1-92}\" y=\"${midTop-42}\" width=\"200\" height=\"42\" fill=\"${C.paper}\"\/>`;\n  labels += `<text class=\"lbl\" style=\"animation-delay:${d}ms\" x=\"${X1+NODE\/2}\" y=\"${midTop-25}\" text-anchor=\"middle\"\n    font-family=\"Plus Jakarta Sans, sans-serif\" font-size=\"15\" font-weight=\"700\" fill=\"${C.ink}\">Haushaltsbudget<\/text>`;\n  labels += `<text class=\"lbl\" style=\"animation-delay:${d}ms\" x=\"${X1+NODE\/2}\" y=\"${midTop-9}\" text-anchor=\"middle\"\n    font-family=\"IBM Plex Mono, monospace\" font-size=\"12\" fill=\"${C.inkSoft}\">${eur(total)} im Monat<\/text>`;\n\n  R.forEach(f=>{\n    nodes += `<rect class=\"nodebar\" style=\"animation-delay:${d}ms\" x=\"${X2}\" y=\"${f.y}\" width=\"${NODE}\" height=\"${Math.max(f.h,2)}\" rx=\"2\" fill=\"${f.c}\"\/>`;\n    const cy = f.y + f.h\/2;\n    const w = f.rest ? 600 : 400;\n    labels += `<text class=\"lbl\" style=\"animation-delay:${d}ms\" x=\"${X2+NODE+13}\" y=\"${cy+4}\"\n      font-family=\"Plus Jakarta Sans, sans-serif\" font-size=\"13\" font-weight=\"${f.rest?600:400}\" fill=\"${C.ink}\">${esc(f.n)}<\/text>`;\n    labels += `<text class=\"lbl\" style=\"animation-delay:${d}ms\" x=\"${W-PAD}\" y=\"${cy+4}\" text-anchor=\"end\"\n      font-family=\"IBM Plex Mono, monospace\" font-size=\"12\" fill=\"${C.inkSoft}\">${eur(f.v)}<\/text>`;\n  });\n\n  const brandY = H - 26;\n  let brand = `<line x1=\"${PAD}\" y1=\"${brandY - 20}\" x2=\"${W - PAD}\" y2=\"${brandY - 20}\" stroke=\"${C.rule}\" stroke-width=\"1\"\/>`;\n  if(BRAND.logo){\n    brand += `<image href=\"${BRAND.logo}\" xlink:href=\"${BRAND.logo}\" x=\"${PAD}\" y=\"${brandY - 14}\"\n      width=\"${BRAND.logoW}\" height=\"${BRAND.logoH}\" preserveAspectRatio=\"xMinYMid meet\"\/>`;\n  } else {\n    brand += `<text x=\"${PAD}\" y=\"${brandY + 6}\" font-family=\"Plus Jakarta Sans, sans-serif\"\n      font-size=\"21\" font-weight=\"800\" letter-spacing=\"-0.6\" fill=\"${C.brand}\">${esc(BRAND.text)}<\/text>`;\n  }\n  brand += `<text x=\"${W - PAD}\" y=\"${brandY + 4}\" text-anchor=\"end\"\n    font-family=\"Plus Jakarta Sans, sans-serif\" font-size=\"11.5\" font-weight=\"600\"\n    letter-spacing=\"0.3\" fill=\"${C.inkSoft}\">${esc(BRAND.claim)}<\/text>`;\n\n  const svg = `<svg viewBox=\"0 0 ${W} ${H}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"\n      xmlns:xlink=\"http:\/\/www.w3.org\/1999\/xlink\" role=\"img\"\n      aria-label=\"Sankey-Diagramm des monatlichen Geldflusses\">\n    <rect width=\"${W}\" height=\"${H}\" fill=\"${C.paper}\"\/>\n    <g>${ribbons}<\/g><g>${nodes}<\/g><g>${labels}<\/g>\n    <g class=\"lbl\" style=\"animation-delay:${d}ms\">${brand}<\/g>\n  <\/svg>`;\n  return {svg, tIn, tOut, rest, H};\n}\n\n\/* ---------- Rendern ---------- *\/\nfunction render(){\n  const {svg, tIn, tOut, rest} = buildSvg();\n  document.getElementById(\"chart\").innerHTML = svg;\n  const spar = DATA.spar.reduce((a,b)=>a+(b[1]||0),0);\n  const ausg = tOut - spar;\n  document.getElementById(\"tIn\").textContent    = eur(tIn);\n  document.getElementById(\"tOut\").textContent   = eur(ausg);\n  document.getElementById(\"tSpar\").textContent  = eur(spar);\n  document.getElementById(\"tRest\").textContent  = eur(rest);\n  document.getElementById(\"tRest\").style.color  = rest < 0 ? C.clay : C.ink;\n  document.getElementById(\"restLbl\").textContent = rest < 0 ? \"Fehlbetrag\" : \"\u00dcberschuss\";\n  document.getElementById(\"tQuote\").textContent = tIn ? Math.round((spar + Math.max(rest,0))\/tIn*100) + \" %\" : \"\u2013\";\n}\n\ndocument.addEventListener(\"input\", e=>{\n  const t = e.target;\n  if(!t.dataset || !t.dataset.kind) return;\n  const key = t.dataset.kind === \"vari\" ? \"vari\" : t.dataset.kind;\n  DATA[key][+t.dataset.i][1] = Math.max(0, parseFloat(t.value) || 0);\n  render();\n});\n\n\/* ---------- Export ---------- *\/\nfunction currentSvgString(){\n  const el = document.querySelector(\"#chart svg\").cloneNode(true);\n  el.querySelectorAll(\".ribbon\").forEach(p=>{ p.removeAttribute(\"class\"); p.removeAttribute(\"style\"); p.setAttribute(\"opacity\",\"0.78\"); });\n  el.querySelectorAll(\".nodebar,.lbl\").forEach(p=>{ p.removeAttribute(\"class\"); p.removeAttribute(\"style\"); });\n  const vb = document.querySelector(\"#chart svg\").viewBox.baseVal;\n  el.setAttribute(\"width\", vb.width);\n  el.setAttribute(\"height\", vb.height);\n  return new XMLSerializer().serializeToString(el);\n}\n\nfunction download(blob, name){\n  const url = URL.createObjectURL(blob);\n  if(EINGEBETTET){\n    \/\/ In der Vorschau greift a.download nicht. Neues Fenster versuchen, sonst ehrlich melden.\n    const neu = window.open(url, \"_blank\", \"noopener\");\n    if(!neu){\n      alert(\"Der Browser blockt das Speichern in dieser Vorschau.\\n\\n\"\n          + \"Lade die Datei Geldfluss-Sankey.html herunter und \u00f6ffne sie per Doppelklick \u2013 \"\n          + \"dann klappen PNG, PDF und SVG.\");\n    }\n    setTimeout(()=>URL.revokeObjectURL(url), 60000);\n    return;\n  }\n  const a = document.createElement(\"a\");\n  a.href = url;\n  a.download = name;\n  a.rel = \"noopener\";\n  document.body.appendChild(a);\n  a.click();\n  a.remove();\n  setTimeout(()=>URL.revokeObjectURL(url), 4000);\n}\n\n\/* ---------- Export ----------\n   Rasterbild aus dem SVG erzeugen. scale = 2 liefert rund 2500 px Breite. *\/\nasync function renderRaster(scale, mime, quality){\n  const src = document.querySelector(\"#chart svg\");\n  const vb = src.viewBox.baseVal;\n  const img = new Image();\n  const url = \"data:image\/svg+xml;charset=utf-8,\" + encodeURIComponent(currentSvgString());\n  await new Promise((res, rej)=>{ img.onload = res; img.onerror = ()=>rej(new Error(\"SVG\")); img.src = url; });\n  const cv = document.createElement(\"canvas\");\n  cv.width = Math.round(vb.width * scale);\n  cv.height = Math.round(vb.height * scale);\n  const cx = cv.getContext(\"2d\");\n  cx.fillStyle = C.paper;\n  cx.fillRect(0, 0, cv.width, cv.height);\n  cx.drawImage(img, 0, 0, cv.width, cv.height);\n  return { canvas: cv, dataUrl: cv.toDataURL(mime, quality), w: vb.width, h: vb.height };\n}\n\n\/* Einseitiges PDF von Hand bauen: das Diagramm als eingebettetes JPEG.\n   Damit braucht die Seite keine externe Bibliothek und funktioniert offline. *\/\nfunction buildPdf(jpegDataUrl, pxW, pxH, ptW, ptH){\n  const bin = atob(jpegDataUrl.split(\",\")[1]);\n  const parts = [];\n  const offsets = [];\n  let len = 0;\n  const push = s => { parts.push(s); len += s.length; };\n  const obj = s => { offsets.push(len); push(s); };\n\n  push(\"%PDF-1.4\\n%\\xE2\\xE3\\xCF\\xD3\\n\");\n  obj(\"1 0 obj\\n<< \/Type \/Catalog \/Pages 2 0 R >>\\nendobj\\n\");\n  obj(\"2 0 obj\\n<< \/Type \/Pages \/Kids [3 0 R] \/Count 1 >>\\nendobj\\n\");\n  obj(\"3 0 obj\\n<< \/Type \/Page \/Parent 2 0 R \/MediaBox [0 0 \" + ptW.toFixed(2) + \" \" + ptH.toFixed(2) + \"]\"\n      + \" \/Resources << \/XObject << \/Im0 5 0 R >> >> \/Contents 4 0 R >>\\nendobj\\n\");\n\n  const content = \"q \" + ptW.toFixed(2) + \" 0 0 \" + ptH.toFixed(2) + \" 0 0 cm \/Im0 Do Q\\n\";\n  obj(\"4 0 obj\\n<< \/Length \" + content.length + \" >>\\nstream\\n\" + content + \"endstream\\nendobj\\n\");\n\n  obj(\"5 0 obj\\n<< \/Type \/XObject \/Subtype \/Image \/Width \" + pxW\n      + \" \/Height \" + pxH + \" \/ColorSpace \/DeviceRGB \/BitsPerComponent 8\"\n      + \" \/Filter \/DCTDecode \/Length \" + bin.length + \" >>\\nstream\\n\" + bin + \"\\nendstream\\nendobj\\n\");\n\n  const xrefPos = len;\n  let xref = \"xref\\n0 6\\n0000000000 65535 f \\n\";\n  offsets.forEach(o => { xref += String(o).padStart(10, \"0\") + \" 00000 n \\n\"; });\n  push(xref);\n  push(\"trailer\\n<< \/Size 6 \/Root 1 0 R >>\\nstartxref\\n\" + xrefPos + \"\\n%%EOF\\n\");\n\n  const all = parts.join(\"\");\n  const bytes = new Uint8Array(all.length);\n  for(let i = 0; i < all.length; i++) bytes[i] = all.charCodeAt(i) % 256;\n  return new Blob([bytes], { type: \"application\/pdf\" });\n}\n\nfunction busy(el, text){\n  el.dataset.old = el.dataset.old || el.querySelector(\".mi-title\").textContent;\n  el.querySelector(\".mi-title\").textContent = text;\n}\nfunction unbusy(el){\n  if(el.dataset.old) el.querySelector(\".mi-title\").textContent = el.dataset.old;\n}\n\ndocument.getElementById(\"dlPng\").onclick = async (e)=>{\n  const it = e.currentTarget;\n  busy(it, \"Bild wird erstellt \u2026\");\n  try{\n    const r = await renderRaster(2, \"image\/png\");\n    await new Promise(res => r.canvas.toBlob(b => { download(b, \"climb-geldfluss.png\"); res(); }, \"image\/png\"));\n  }catch(err){ busy(it, \"Hat nicht geklappt \u2013 nimm SVG\"); await new Promise(r=>setTimeout(r,2200)); }\n  unbusy(it);\n  closeMenu();\n};\n\ndocument.getElementById(\"dlPdf\").onclick = async (e)=>{\n  const it = e.currentTarget;\n  busy(it, \"PDF wird erstellt \u2026\");\n  try{\n    const r = await renderRaster(4, \"image\/jpeg\", 0.94);\n    \/\/ Querformat auf A4-Breite: 842 pt lange Kante\n    const ptW = 842;\n    const ptH = 842 * (r.h \/ r.w);\n    download(buildPdf(r.dataUrl, r.canvas.width, r.canvas.height, ptW, ptH), \"climb-geldfluss.pdf\");\n  }catch(err){ busy(it, \"Hat nicht geklappt \u2013 nimm PNG\"); await new Promise(r=>setTimeout(r,2200)); }\n  unbusy(it);\n  closeMenu();\n};\n\ndocument.getElementById(\"dlSvg\").onclick = ()=>{\n  download(new Blob([currentSvgString()], {type:\"image\/svg+xml\"}), \"climb-geldfluss.svg\");\n  closeMenu();\n};\n\nconst dlMenu = document.getElementById(\"dlMenu\");\nconst dlBtn = document.getElementById(\"btnDl\");\nfunction closeMenu(){ dlMenu.classList.remove(\"open\"); dlBtn.setAttribute(\"aria-expanded\",\"false\"); }\ndlBtn.onclick = ()=>{\n  const open = dlMenu.classList.toggle(\"open\");\n  dlBtn.setAttribute(\"aria-expanded\", open ? \"true\" : \"false\");\n};\ndocument.addEventListener(\"click\", e => { if(!dlMenu.contains(e.target)) closeMenu(); });\ndocument.addEventListener(\"keydown\", e => { if(e.key === \"Escape\") closeMenu(); });\n\n\/* ---------- Permalink ----------\n   Zustand steckt im URL-Fragment: #v1=<ein>|<fix>|<var>|<spar>\n   Pro Gruppe die Betr\u00e4ge in fester Reihenfolge, durch Punkt getrennt, 0 bleibt leer.\n   Beispiel: #v1=2400.1600...250|1150.210|620||300\n   Wird die Datei sp\u00e4ter auf goclimb.com gehostet, ist das ein teilbarer Link. *\/\nconst ORDER = [\"ein\",\"fix\",\"vari\",\"spar\"];\n\nfunction stateToHash(){\n  \/\/ Nur Zeichen verwenden, die in URLs erlaubt sind: Ziffern, Punkt, Tilde.\n  \/\/ Das Pipe-Zeichen \"|\" ist unzul\u00e4ssig \u2013 Mail- und Chatprogramme k\u00fcrzen den Link dort ab.\n  return \"#v1=\" + ORDER.map(k =>\n    DATA[k].map(r => r[1] ? String(Math.round(r[1] * 100) \/ 100) : \"\").join(\".\")\n  ).join(\"~\");\n}\n\nfunction hashToState(hash){\n  let raw = hash || \"\";\n  try{ raw = decodeURIComponent(raw); }catch(e){}\n  const m = \/^#v1=(.*)$\/.exec(raw);\n  if(!m) return false;\n  const groups = m[1].split(\/[~|]\/);   \/\/ \"|\" wird noch gelesen, damit \u00e4ltere Links weiter aufgehen\n  let any = false;\n  \/\/ erst alles auf null, sonst blieben Vorgabewerte stehen, die nicht im Link stecken\n  ORDER.forEach(k => DATA[k].forEach(r => r[1] = 0));\n  ORDER.forEach((k, gi) => {\n    if(groups[gi] === undefined) return;\n    groups[gi].split(\".\").forEach((v, i) => {\n      if(!DATA[k][i]) return;\n      const n = v === \"\" ? 0 : parseFloat(v);\n      if(isFinite(n)){ DATA[k][i][1] = Math.max(0, n); any = true; }\n    });\n  });\n  return any;\n}\n\nfunction syncInputs(){\n  ORDER.forEach(k => DATA[k].forEach((r, i) => {\n    const el = document.getElementById(k + i);\n    if(el) el.value = r[1] ? r[1] : \"\";\n  }));\n}\n\nfunction currentPermalink(){\n  \/\/ location.origin ist bei lokalen Dateien \"null\" \u2013 deshalb \u00fcber href gehen.\n  return location.href.split(\"#\")[0] + stateToHash();\n}\n\nasync function copyText(txt){\n  if(navigator.clipboard){\n    if(window.isSecureContext){\n      try{ await navigator.clipboard.writeText(txt); return true; }catch(e){}\n    }\n  }\n  const ta = document.createElement(\"textarea\");\n  ta.value = txt;\n  ta.setAttribute(\"readonly\",\"\");\n  ta.style.cssText = \"position:fixed;top:-1000px;opacity:0\";\n  document.body.appendChild(ta);\n  ta.select();\n  ta.setSelectionRange(0, ta.value.length);\n  let ok = false;\n  try{ ok = document.execCommand(\"copy\"); }catch(e){}\n  ta.remove();\n  return ok;\n}\n\ndocument.getElementById(\"btnLink\").onclick = async (ev)=>{\n  const btn = ev.currentTarget;\n  const label = btn.dataset.label || btn.textContent;\n  btn.dataset.label = label;\n\n  const url = currentPermalink();\n  try{ history.replaceState(null, \"\", stateToHash()); }catch(e){ location.hash = stateToHash(); }\n\n  const box = document.getElementById(\"linkBox\");\n  const field = document.getElementById(\"linkField\");\n  field.value = url;\n  box.hidden = false;\n\n  const ok = await copyText(url);\n  btn.textContent = ok ? \"Link kopiert\" : \"Link steht unten\";\n  document.getElementById(\"linkNote\").textContent = EINGEBETTET\n    ? \"In dieser Vorschau sperrt der Browser die Zwischenablage. Markiere den Link oben und kopiere ihn mit Strg+C.\"\n    : ok\n    ? (location.protocol === \"file:\"\n        ? \"Kopiert. Achtung: Die Seite liegt noch lokal auf deinem Rechner, deshalb beginnt der Link mit file:\/\/\/ und \u00f6ffnet sich nur bei dir. Sobald die Seite auf goclimb.com liegt, wird daraus ein teilbarer Link.\"\n        : \"Der Link liegt in der Zwischenablage und enth\u00e4lt alle eingetragenen Betr\u00e4ge.\")\n    : \"Kopieren hat der Browser geblockt \u2013 markiere den Link unten und kopiere ihn mit Strg+C.\";\n  if(!ok){ field.focus(); field.select(); }\n  setTimeout(()=>{ btn.textContent = label; }, 2600);\n};\n\ndocument.getElementById(\"btnClear\").onclick = ()=>{\n  [\"ein\",\"fix\",\"vari\",\"spar\"].forEach(k => DATA[k].forEach(r => r[1] = 0));\n  document.querySelectorAll(\"input[data-kind]\").forEach(i => i.value = \"\");\n  render();\n};\n\nconst norm = s => String(s).toLowerCase()\n  .replace(\/[\u2013\u2014]\/g,\"-\").replace(\/[^a-z0-9\u00e4\u00f6\u00fc\u00df]\/g,\" \")\n  .replace(\/\\s+\/g,\" \").trim();\n\nfunction parseAmount(s){\n  s = String(s).replace(\/[\u20ac\\s ]\/g,\"\");\n  if(!s) return null;\n  const hasComma = s.includes(\",\");\n  const hasDot = s.includes(\".\");\n  if(hasComma){\n    if(hasDot) s = s.replace(\/\\.\/g,\"\").replace(\",\",\".\");\n    else s = s.replace(\",\",\".\");\n  }\n  const v = parseFloat(s);\n  return isFinite(v) ? v : null;\n}\n\nfunction applyImport(text){\n  const index = {};\n  [\"ein\",\"fix\",\"vari\",\"spar\"].forEach(k =>\n    DATA[k].forEach((r,i) => { index[norm(r[0])] = [k,i]; }));\n\n  let hit = 0, miss = [];\n  text.split(\/\\r?\\n\/).forEach(line => {\n    if(!line.trim()) return;\n    const parts = line.split(\/\\t|;\/);\n    if(parts.length < 2) return;\n    const name = norm(parts[0]);\n    const val = parseAmount(parts[parts.length-1]);\n    if(val === null) return;\n    if(index[name]){\n      const [k,i] = index[name];\n      DATA[k][i][1] = Math.max(0, val);\n      hit++;\n    } else if(parts[0].trim()) {\n      miss.push(parts[0].trim());\n    }\n  });\n\n  const note = document.getElementById(\"importNote\");\n  if(hit){\n    syncInputs();\n    render();\n    note.className = \"importnote ok\";\n    note.textContent = hit + \" Positionen \u00fcbernommen\"\n      + (miss.length ? \" \u00b7 nicht zugeordnet: \" + miss.slice(0,3).join(\", \") + (miss.length>3 ? \" \u2026\" : \"\") : \"\");\n  } else {\n    note.className = \"importnote warn\";\n    note.textContent = \"Nichts erkannt. Erwartet werden zwei Spalten: Bezeichnung und Betrag.\";\n  }\n}\n\nconst pasteEl = document.getElementById(\"paste\");\npasteEl.addEventListener(\"paste\", e => {\n  const t = (e.clipboardData || window.clipboardData).getData(\"text\");\n  setTimeout(() => applyImport(t), 0);\n});\npasteEl.addEventListener(\"change\", () => applyImport(pasteEl.value));\n\nif(hashToState(location.hash)){\n  document.title = \"Geldfluss \u2013 geteilte Berechnung\";\n}\n\n\/* In einem eingebetteten Rahmen sperrt der Browser Downloads und Zwischenablage.\n   Dann Hinweis zeigen statt den Nutzer ins Leere laufen zu lassen. *\/\nconst EINGEBETTET = (function(){\n  try { return window.self !== window.top; } catch(e) { return true; }\n})();\n\nif(EINGEBETTET){\n  const w = document.getElementById(\"frameWarn\");\n  w.hidden = false;\n  document.getElementById(\"popOut\").onclick = ()=>{\n    const neu = window.open(location.href, \"_blank\", \"noopener\");\n    if(!neu) alert(\"Der Browser hat auch das geblockt. Bitte die HTML-Datei herunterladen und per Doppelklick \u00f6ffnen.\");\n  };\n}\n\nbuildList(\"listIn\",\"ein\");\nbuildList(\"listFix\",\"fix\");\nbuildList(\"listVar\",\"vari\");\nbuildList(\"listSpar\",\"spar\");\nrender();\n\nwindow.addEventListener(\"hashchange\", ()=>{\n  if(hashToState(location.hash)){ syncInputs(); render(); }\n});\n<\/script>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Dein Geldfluss Trag die Betr\u00e4ge eines Monats ein \u2014 das Diagramm zeichnet sich beim Tippen neu. Am Ende als Bild, [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"default","adv-header-id-meta":"153","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"class_list":["post-800","page","type-page","status-publish","hentry"],"acf":[],"_links":{"self":[{"href":"https:\/\/goclimb.com\/en\/wp-json\/wp\/v2\/pages\/800","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/goclimb.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/goclimb.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/goclimb.com\/en\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/goclimb.com\/en\/wp-json\/wp\/v2\/comments?post=800"}],"version-history":[{"count":10,"href":"https:\/\/goclimb.com\/en\/wp-json\/wp\/v2\/pages\/800\/revisions"}],"predecessor-version":[{"id":821,"href":"https:\/\/goclimb.com\/en\/wp-json\/wp\/v2\/pages\/800\/revisions\/821"}],"wp:attachment":[{"href":"https:\/\/goclimb.com\/en\/wp-json\/wp\/v2\/media?parent=800"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}