mantenimento-app 2.1.8 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.js CHANGED
@@ -26,7 +26,9 @@ const defaultExpenseItems = [
26
26
  let selectedScenarioIdx = -1;
27
27
  let scenarioTransitionTimer = null;
28
28
  const SCENARIO_LAB_MAX = 3;
29
- const SCENARIO_LABELS = ["A", "B", "C"];
29
+ const SCENARIO_LAB_MAX_FREE = 3;
30
+ const SCENARIO_LAB_MAX_DONOR = 6;
31
+ const SCENARIO_LABELS = ["A", "B", "C", "D", "E", "F"];
30
32
  const EXPENSE_DETAIL_MAX_CHARS = 560;
31
33
  const EXPENSE_DETAIL_MAX_LINES = 10;
32
34
 
@@ -134,6 +136,11 @@ const defaultExpenseItems = [
134
136
  authUserFallback: "utente",
135
137
  authLogoutDone: "Logout eseguito.",
136
138
  authLoginRequired: "Effettua prima il login.",
139
+ gateNeedsAuth: "Funzionalità riservata agli utenti registrati. Accedi o registrati per continuare.",
140
+ gateNeedsAuthTitle: "Accesso richiesto",
141
+ gateNeedsDonor: "Funzionalità Premium riservata ai donatori. Supporta il progetto per sbloccarla.",
142
+ gateNeedsDonorTitle: "Funzionalità Premium",
143
+ gateDonorBadge: "Premium",
137
144
  authSaveFailed: "Salvataggio profilo fallito: {message}",
138
145
  authCloudSaved: "Profilo cloud salvato. Versioni storiche: {count}.",
139
146
  authLoadFailed: "Caricamento profilo fallito: {message}",
@@ -367,6 +374,19 @@ const defaultExpenseItems = [
367
374
  spiegDetailPerm: "La quota diretta dipende dai giorni di permanenza presso ciascun coniuge: quota diretta = fabbisogno x permanenza%.",
368
375
  spiegDetailResultTransfer: "L'assegno suggerito nasce dal saldo: quota teorica del pagante - quota diretta del pagante. Se il saldo e positivo, viene trasferito all'altro coniuge.",
369
376
  spiegDetailResultNoTransfer: "Se nessun saldo risulta positivo, il modello non suggerisce trasferimenti tra coniugi.",
377
+ sepCostBoxTitle: "💸 Costo della separazione",
378
+ sepCostBoxNote: "Stima le spese mensili che la coppia avrebbe sostenuto vivendo insieme: il modello calcolerà il costo di duplicazione generato dalla separazione e la perdita economica netta per ciascun coniuge.",
379
+ lblSpeseConvivenza: "Spese mensili stimate in convivenza ({currency})",
380
+ hintSpeseConvivenza: "Stima delle spese mensili totali della coppia se non si fosse separata: affitto/mutuo unico, una sola utenza, una sola auto, ecc. Lascia 0 per non includere questa analisi.",
381
+ sepCostDuplication: "Costo duplicazione mensile",
382
+ sepCostPanelTitle: "💔 Effetto economico della separazione",
383
+ sepCostNetTogether: "Netto combinato se insieme",
384
+ sepCostNetSeparated: "Netto combinato dopo separazione",
385
+ sepCostLossMonthly: "Perdita economica mensile",
386
+ sepCostLossAnnually: "Perdita economica annua",
387
+ sepCostLossSpouse: "Impatto stimato su {spouse}",
388
+ sepCostInlineHint: "Duplicazione mensile stimata: {amount}",
389
+ sepCostWarning: "Inserisci le spese mensili in convivenza nel campo sopra per attivare questa analisi.",
370
390
  footerVisitorsTotal: "Visitatori totali",
371
391
  footerVisitorsActive: "Visitatori attivi",
372
392
  footerLoggedUsers: "Utenti loggati",
@@ -455,6 +475,11 @@ const defaultExpenseItems = [
455
475
  authUserFallback: "user",
456
476
  authLogoutDone: "Logout completed.",
457
477
  authLoginRequired: "Please login first.",
478
+ gateNeedsAuth: "This feature is for registered users only. Log in or sign up to continue.",
479
+ gateNeedsAuthTitle: "Login required",
480
+ gateNeedsDonor: "Premium feature for donors. Support the project to unlock it.",
481
+ gateNeedsDonorTitle: "Premium feature",
482
+ gateDonorBadge: "Premium",
458
483
  authSaveFailed: "Cloud profile save failed: {message}",
459
484
  authCloudSaved: "Cloud profile saved. History versions: {count}.",
460
485
  authLoadFailed: "Cloud profile load failed: {message}",
@@ -688,6 +713,19 @@ const defaultExpenseItems = [
688
713
  spiegDetailPerm: "Direct share depends on permanence days with each spouse: direct share = children needs x permanence%.",
689
714
  spiegDetailResultTransfer: "Suggested support comes from the balance: payer theoretical share - payer direct share. If the balance is positive, it is transferred to the other spouse.",
690
715
  spiegDetailResultNoTransfer: "If no balance is positive, the model suggests no transfer between spouses.",
716
+ sepCostBoxTitle: "💸 Cost of separation",
717
+ sepCostBoxNote: "Estimate the monthly expenses the couple would have incurred if living together: the model will compute the duplication cost generated by the separation and the net economic loss per spouse.",
718
+ lblSpeseConvivenza: "Estimated monthly expenses when cohabiting ({currency})",
719
+ hintSpeseConvivenza: "Estimated total monthly expenses of the couple if they had not separated: single rent/mortgage, single utilities, single car, etc. Leave 0 to skip this analysis.",
720
+ sepCostDuplication: "Monthly duplication cost",
721
+ sepCostPanelTitle: "💔 Economic effect of separation",
722
+ sepCostNetTogether: "Combined net if together",
723
+ sepCostNetSeparated: "Combined net after separation",
724
+ sepCostLossMonthly: "Monthly economic loss",
725
+ sepCostLossAnnually: "Annual economic loss",
726
+ sepCostLossSpouse: "Estimated impact on {spouse}",
727
+ sepCostInlineHint: "Estimated monthly duplication: {amount}",
728
+ sepCostWarning: "Enter the cohabiting monthly expenses above to activate this analysis.",
691
729
  footerVisitorsTotal: "Total visitors",
692
730
  footerVisitorsActive: "Active visitors",
693
731
  footerLoggedUsers: "Logged users",
@@ -710,7 +748,8 @@ const defaultExpenseItems = [
710
748
  const authSession = {
711
749
  username: null,
712
750
  userId: null,
713
- keyBits: null
751
+ keyBits: null,
752
+ isDonor: false
714
753
  };
715
754
  const authUiState = {
716
755
  mode: "login",
@@ -1171,6 +1210,14 @@ const defaultExpenseItems = [
1171
1210
  if (permLegendC2) permLegendC2.textContent = c2n();
1172
1211
  if (extraBoxTitle) extraBoxTitle.textContent = tr("extraBoxTitle");
1173
1212
  if (extraBoxNote) extraBoxNote.textContent = tr("extraBoxNote");
1213
+ const sepCostBoxTitleEl = document.getElementById("sepCostBoxTitle");
1214
+ const sepCostBoxNoteEl = document.getElementById("sepCostBoxNote");
1215
+ const lblSpeseConvivenzaEl = document.getElementById("lblSpeseConvivenza");
1216
+ const hintSpeseConvivenzaEl = document.getElementById("hintSpeseConvivenza");
1217
+ if (sepCostBoxTitleEl) sepCostBoxTitleEl.textContent = tr("sepCostBoxTitle");
1218
+ if (sepCostBoxNoteEl) sepCostBoxNoteEl.textContent = tr("sepCostBoxNote");
1219
+ if (lblSpeseConvivenzaEl) lblSpeseConvivenzaEl.textContent = msg("lblSpeseConvivenza", { currency: currentCurrency });
1220
+ if (hintSpeseConvivenzaEl) hintSpeseConvivenzaEl.title = tr("hintSpeseConvivenza");
1174
1221
  if (firstHomeBoxTitle) firstHomeBoxTitle.textContent = tr("firstHomeBoxTitle");
1175
1222
  if (firstHomeBoxNote) firstHomeBoxNote.textContent = tr("firstHomeBoxNote");
1176
1223
  if (lblPrimaCasaMutuoEnabled) lblPrimaCasaMutuoEnabled.textContent = tr("firstHomeMortgageEnabledLabel");
@@ -1642,6 +1689,7 @@ const defaultExpenseItems = [
1642
1689
  authSession.username = username;
1643
1690
  authSession.userId = user.id;
1644
1691
  authSession.keyBits = await deriveSessionKeyBits(password, user.id);
1692
+ authSession.isDonor = localStorage.getItem(`m_donor_${user.id}`) === "1";
1645
1693
  updateAuthUi();
1646
1694
  return msg("authLoginAs", { username });
1647
1695
  }
@@ -1766,6 +1814,55 @@ const defaultExpenseItems = [
1766
1814
  el.textContent = message;
1767
1815
  }
1768
1816
 
1817
+ function isLoggedIn() { return !!authSession.username; }
1818
+ function isDonationPolicyBypassedUser() {
1819
+ return normalizeUsername(authSession.username) === "favagit";
1820
+ }
1821
+ function isDonorUser() {
1822
+ return !!authSession.isDonor || isDonationPolicyBypassedUser();
1823
+ }
1824
+
1825
+ function getScenarioMaxForUser() {
1826
+ if (!isLoggedIn()) return 0;
1827
+ return isDonorUser() ? SCENARIO_LAB_MAX_DONOR : SCENARIO_LAB_MAX_FREE;
1828
+ }
1829
+
1830
+ function showAuthGateMessage(triggerEl) {
1831
+ setAuthMenuOpen(true);
1832
+ setAuthStatus(tr("gateNeedsAuth"), true);
1833
+ if (triggerEl) {
1834
+ triggerEl.classList.add("gate-flash");
1835
+ setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
1836
+ }
1837
+ }
1838
+
1839
+ function showDonorGateMessage(triggerEl) {
1840
+ setAuthMenuOpen(true);
1841
+ setAuthStatus(tr("gateNeedsDonor"), true);
1842
+ if (triggerEl) {
1843
+ triggerEl.classList.add("gate-flash");
1844
+ setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
1845
+ }
1846
+ const donateBanner = document.querySelector(".donate-banner");
1847
+ if (donateBanner) setTimeout(() => donateBanner.scrollIntoView({ behavior: "smooth", block: "center" }), 200);
1848
+ }
1849
+
1850
+ function applyFeatureGates() {
1851
+ const logged = isLoggedIn();
1852
+ const donor = isDonorUser();
1853
+ const authGatedIds = ["btnPdf", "btnExportJson", "btnImportJson", "btnSaveScenario"];
1854
+ authGatedIds.forEach((id) => {
1855
+ const btn = document.getElementById(id);
1856
+ if (!btn) return;
1857
+ btn.classList.toggle("gate-locked", !logged);
1858
+ btn.classList.toggle("gate-donor", logged && !donor && id === "btnSaveScenario" && false); // donor gate placeholder
1859
+ });
1860
+ const authMenuBtn = document.getElementById("btnAuthMenu");
1861
+ if (authMenuBtn) {
1862
+ authMenuBtn.classList.toggle("has-donor-badge", logged && donor);
1863
+ }
1864
+ }
1865
+
1769
1866
  function updateAuthModeUi() {
1770
1867
  const isSignup = authUiState.mode === "signup";
1771
1868
  const loginModeBtn = document.getElementById("btnAuthModeLogin");
@@ -1809,7 +1906,8 @@ const defaultExpenseItems = [
1809
1906
  if (sessionActions) sessionActions.classList.toggle("is-hidden", !logged);
1810
1907
  if (toggleBtn) {
1811
1908
  toggleBtn.classList.toggle("logged", logged);
1812
- toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}` : tr("authLogin");
1909
+ const badge = logged && authSession.isDonor ? ` ✦` : "";
1910
+ toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}${badge}` : tr("authLogin");
1813
1911
  }
1814
1912
 
1815
1913
  if (logged) {
@@ -1833,6 +1931,7 @@ const defaultExpenseItems = [
1833
1931
  setAuthStatus(tr("authNotAuthenticated"), false);
1834
1932
  }
1835
1933
 
1934
+ applyFeatureGates();
1836
1935
  void syncPresenceTrackState();
1837
1936
  }
1838
1937
 
@@ -2377,6 +2476,7 @@ const defaultExpenseItems = [
2377
2476
  authSession.username = null;
2378
2477
  authSession.userId = null;
2379
2478
  authSession.keyBits = null;
2479
+ authSession.isDonor = false;
2380
2480
  cloudProfileSession.loaded = null;
2381
2481
  cloudProfileSession.history = [];
2382
2482
  updateAuthUi();
@@ -3202,7 +3302,8 @@ const defaultExpenseItems = [
3202
3302
  straordAnn1: num("straordAnn1"),
3203
3303
  straordAnn2: num("straordAnn2"),
3204
3304
  c1SpeseDetails,
3205
- c2SpeseDetails,
3305
+ c2SpeseDetails,
3306
+ speseConvivenza: num("speseConvivenza"),
3206
3307
  c1SpeseDetailUi,
3207
3308
  c2SpeseDetailUi,
3208
3309
  c1Spese,
@@ -3323,7 +3424,18 @@ const defaultExpenseItems = [
3323
3424
  const post1 = disp1 - assegnoDa1a2 + assegnoDa2a1;
3324
3425
  const post2 = disp2 - assegnoDa2a1 + assegnoDa1a2;
3325
3426
 
3326
- return {
3427
+ // Separation cost analysis (only active when speseConvivenza > 0)
3428
+ const speseConvivenza = Math.max(0, Number(payload.speseConvivenza || 0));
3429
+ const costoSeparazioneMensile = speseConvivenza > 0 ? speseTot - speseConvivenza : null;
3430
+ const nettoInsiemeCombinato = speseConvivenza > 0 ? (r1 + r2 - speseConvivenza) : null;
3431
+ const nettoSeparatoTotale = post1 + post2;
3432
+ const perditaMensile = nettoInsiemeCombinato !== null ? nettoInsiemeCombinato - nettoSeparatoTotale : null;
3433
+ const perditaAnnua = perditaMensile !== null ? perditaMensile * 12 : null;
3434
+ const totReddito = Math.max(0.001, r1 + r2);
3435
+ const perditaSpouse1 = perditaMensile !== null ? perditaMensile * (r1 / totReddito) : null;
3436
+ const perditaSpouse2 = perditaMensile !== null ? perditaMensile * (r2 / totReddito) : null;
3437
+
3438
+ return {
3327
3439
  r1, r2, r1Raw, r2Raw, incomeMode, figli, perm1, perm2,
3328
3440
  aPerc1, aPag1, aPerc2, aPag2, aFam1, aFam2,
3329
3441
  match12, match21, esternoPag1, esternoPag2,
@@ -3341,7 +3453,11 @@ const defaultExpenseItems = [
3341
3453
  primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
3342
3454
  compensativeBenefits,
3343
3455
  assegnoDa1a2, assegnoDa2a1,
3344
- post1, post2
3456
+ post1, post2,
3457
+ speseConvivenza, costoSeparazioneMensile,
3458
+ nettoInsiemeCombinato, nettoSeparatoTotale,
3459
+ perditaMensile, perditaAnnua,
3460
+ perditaSpouse1, perditaSpouse2
3345
3461
  };
3346
3462
  }
3347
3463
 
@@ -3930,7 +4046,7 @@ const defaultExpenseItems = [
3930
4046
  }
3931
4047
 
3932
4048
  function saveCurrentScenario() {
3933
- if (scenarioLab.length >= SCENARIO_LAB_MAX) {
4049
+ if (scenarioLab.length >= getScenarioMaxForUser()) {
3934
4050
  alert(tr("scenarioLabMaxReached"));
3935
4051
  return;
3936
4052
  }
@@ -4465,6 +4581,57 @@ const defaultExpenseItems = [
4465
4581
  });
4466
4582
  }
4467
4583
 
4584
+ function renderSeparationCostPanel(m) {
4585
+ const panel = document.getElementById("sepCostPanel");
4586
+ if (!panel) return;
4587
+
4588
+ // Update inline hint showing duplication cost
4589
+ const hintDiv = document.getElementById("speseConvivenzaHint");
4590
+ if (hintDiv) {
4591
+ hintDiv.textContent = m.speseConvivenza > 0 && m.costoSeparazioneMensile !== null
4592
+ ? msg("sepCostInlineHint", { amount: eur(m.costoSeparazioneMensile) })
4593
+ : "";
4594
+ }
4595
+
4596
+ if (!m.speseConvivenza || m.speseConvivenza <= 0) {
4597
+ panel.innerHTML = `<div class="sep-cost-warning">${escapeHtml(tr("sepCostWarning"))}</div>`;
4598
+ return;
4599
+ }
4600
+
4601
+ const c1Name = c1n();
4602
+ const c2Name = c2n();
4603
+ const c1NameEsc = escapeHtml(c1Name);
4604
+ const c2NameEsc = escapeHtml(c2Name);
4605
+
4606
+ const lossClass = (v) => v === null ? "" : v > 0 ? "sep-loss-negative" : "sep-loss-positive";
4607
+ const rowHtml = (label, value, em) => {
4608
+ const cls = lossClass(value);
4609
+ const formatted = value === null ? "&mdash;" : eur(value);
4610
+ return `<div class="sep-cost-row${em ? " sep-cost-row--em" : ""}">
4611
+ <span class="sep-cost-label">${label}</span>
4612
+ <strong class="sep-cost-value ${cls}">${formatted}</strong>
4613
+ </div>`;
4614
+ };
4615
+
4616
+ panel.innerHTML = `
4617
+ <div class="sep-cost-panel">
4618
+ <h3 class="sep-cost-title">${escapeHtml(tr("sepCostPanelTitle"))}</h3>
4619
+ <div class="sep-cost-section">
4620
+ ${rowHtml(tr("sepCostNetTogether"), m.nettoInsiemeCombinato, false)}
4621
+ ${rowHtml(tr("sepCostNetSeparated"), m.nettoSeparatoTotale, false)}
4622
+ </div>
4623
+ <div class="sep-cost-divider"></div>
4624
+ <div class="sep-cost-section">
4625
+ ${rowHtml(tr("sepCostDuplication"), m.costoSeparazioneMensile, false)}
4626
+ ${rowHtml(tr("sepCostLossMonthly"), m.perditaMensile, true)}
4627
+ ${rowHtml(tr("sepCostLossAnnually"), m.perditaAnnua, true)}
4628
+ ${rowHtml(msg("sepCostLossSpouse", { spouse: c1NameEsc }), m.perditaSpouse1, false)}
4629
+ ${rowHtml(msg("sepCostLossSpouse", { spouse: c2NameEsc }), m.perditaSpouse2, false)}
4630
+ </div>
4631
+ </div>
4632
+ `;
4633
+ }
4634
+
4468
4635
  function renderAll() {
4469
4636
  const m = computeModel();
4470
4637
  updateExtraordinaryModuleUi();
@@ -4472,6 +4639,7 @@ const defaultExpenseItems = [
4472
4639
  renderLivePanel(m);
4473
4640
  calculate(m);
4474
4641
  renderSpiegabilita(m);
4642
+ renderSeparationCostPanel(m);
4475
4643
  renderScenarioLab();
4476
4644
  }
4477
4645
 
@@ -5303,7 +5471,8 @@ ${scenarioLab.length ? `
5303
5471
  primaCasaAssegnataA: String(document.getElementById("primaCasaAssegnataA")?.value || ""),
5304
5472
  primaCasaMutuoPerc1: num("primaCasaMutuoPerc1"),
5305
5473
  straordAnn1: num("straordAnn1"),
5306
- straordAnn2: num("straordAnn2")
5474
+ straordAnn2: num("straordAnn2"),
5475
+ speseConvivenza: num("speseConvivenza")
5307
5476
  };
5308
5477
  const spese = expenseItems.map((_, i) => ({
5309
5478
  c1: num(`c1_${i}`),
@@ -5438,10 +5607,12 @@ ${scenarioLab.length ? `
5438
5607
  document.getElementById("btnReset").addEventListener("click", resetAll);
5439
5608
 
5440
5609
  document.getElementById("btnExportJson").addEventListener("click", async () => {
5610
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnExportJson")); return; }
5441
5611
  await exportJson();
5442
5612
  });
5443
5613
 
5444
5614
  document.getElementById("btnImportJson").addEventListener("click", () => {
5615
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnImportJson")); return; }
5445
5616
  document.getElementById("fileJson").click();
5446
5617
  });
5447
5618
 
@@ -5454,10 +5625,12 @@ ${scenarioLab.length ? `
5454
5625
  });
5455
5626
 
5456
5627
  document.getElementById("btnPdf").addEventListener("click", () => {
5628
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnPdf")); return; }
5457
5629
  exportPdfDirect();
5458
5630
  });
5459
5631
 
5460
5632
  document.getElementById("btnSaveScenario").addEventListener("click", () => {
5633
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnSaveScenario")); return; }
5461
5634
  saveCurrentScenario();
5462
5635
  });
5463
5636
 
@@ -125,6 +125,17 @@ function calculateModel(input) {
125
125
  const post1 = disp1 - assegnoDa1a2 + assegnoDa2a1;
126
126
  const post2 = disp2 - assegnoDa2a1 + assegnoDa1a2;
127
127
 
128
+ // Separation cost analysis (only active when speseConvivenza > 0)
129
+ const speseConvivenza = Math.max(0, toNumber(input.speseConvivenza));
130
+ const costoSeparazioneMensile = speseConvivenza > 0 ? speseTot - speseConvivenza : null;
131
+ const nettoInsiemeCombinato = speseConvivenza > 0 ? (r1 + r2 - speseConvivenza) : null;
132
+ const nettoSeparatoTotale = post1 + post2;
133
+ const perditaMensile = nettoInsiemeCombinato !== null ? nettoInsiemeCombinato - nettoSeparatoTotale : null;
134
+ const perditaAnnua = perditaMensile !== null ? perditaMensile * 12 : null;
135
+ const totReddito = Math.max(0.001, r1 + r2);
136
+ const perditaSpouse1 = perditaMensile !== null ? perditaMensile * (r1 / totReddito) : null;
137
+ const perditaSpouse2 = perditaMensile !== null ? perditaMensile * (r2 / totReddito) : null;
138
+
128
139
  return {
129
140
  r1, r2, r1Raw, r2Raw, incomeMode, figli, perm1, perm2,
130
141
  aPerc1, aPag1, aPerc2, aPag2, aFam1, aFam2,
@@ -144,7 +155,11 @@ function calculateModel(input) {
144
155
  primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
145
156
  compensativeBenefits,
146
157
  assegnoDa1a2, assegnoDa2a1,
147
- post1, post2
158
+ post1, post2,
159
+ speseConvivenza, costoSeparazioneMensile,
160
+ nettoInsiemeCombinato, nettoSeparatoTotale,
161
+ perditaMensile, perditaAnnua,
162
+ perditaSpouse1, perditaSpouse2
148
163
  };
149
164
  }
150
165
 
@@ -26,7 +26,9 @@ const defaultExpenseItems = [
26
26
  let selectedScenarioIdx = -1;
27
27
  let scenarioTransitionTimer = null;
28
28
  const SCENARIO_LAB_MAX = 3;
29
- const SCENARIO_LABELS = ["A", "B", "C"];
29
+ const SCENARIO_LAB_MAX_FREE = 3;
30
+ const SCENARIO_LAB_MAX_DONOR = 6;
31
+ const SCENARIO_LABELS = ["A", "B", "C", "D", "E", "F"];
30
32
  const EXPENSE_DETAIL_MAX_CHARS = 560;
31
33
  const EXPENSE_DETAIL_MAX_LINES = 10;
32
34
 
@@ -134,6 +136,11 @@ const defaultExpenseItems = [
134
136
  authUserFallback: "utente",
135
137
  authLogoutDone: "Logout eseguito.",
136
138
  authLoginRequired: "Effettua prima il login.",
139
+ gateNeedsAuth: "Funzionalità riservata agli utenti registrati. Accedi o registrati per continuare.",
140
+ gateNeedsAuthTitle: "Accesso richiesto",
141
+ gateNeedsDonor: "Funzionalità Premium riservata ai donatori. Supporta il progetto per sbloccarla.",
142
+ gateNeedsDonorTitle: "Funzionalità Premium",
143
+ gateDonorBadge: "Premium",
137
144
  authSaveFailed: "Salvataggio profilo fallito: {message}",
138
145
  authCloudSaved: "Profilo cloud salvato. Versioni storiche: {count}.",
139
146
  authLoadFailed: "Caricamento profilo fallito: {message}",
@@ -367,6 +374,19 @@ const defaultExpenseItems = [
367
374
  spiegDetailPerm: "La quota diretta dipende dai giorni di permanenza presso ciascun coniuge: quota diretta = fabbisogno x permanenza%.",
368
375
  spiegDetailResultTransfer: "L'assegno suggerito nasce dal saldo: quota teorica del pagante - quota diretta del pagante. Se il saldo e positivo, viene trasferito all'altro coniuge.",
369
376
  spiegDetailResultNoTransfer: "Se nessun saldo risulta positivo, il modello non suggerisce trasferimenti tra coniugi.",
377
+ sepCostBoxTitle: "💸 Costo della separazione",
378
+ sepCostBoxNote: "Stima le spese mensili che la coppia avrebbe sostenuto vivendo insieme: il modello calcolerà il costo di duplicazione generato dalla separazione e la perdita economica netta per ciascun coniuge.",
379
+ lblSpeseConvivenza: "Spese mensili stimate in convivenza ({currency})",
380
+ hintSpeseConvivenza: "Stima delle spese mensili totali della coppia se non si fosse separata: affitto/mutuo unico, una sola utenza, una sola auto, ecc. Lascia 0 per non includere questa analisi.",
381
+ sepCostDuplication: "Costo duplicazione mensile",
382
+ sepCostPanelTitle: "💔 Effetto economico della separazione",
383
+ sepCostNetTogether: "Netto combinato se insieme",
384
+ sepCostNetSeparated: "Netto combinato dopo separazione",
385
+ sepCostLossMonthly: "Perdita economica mensile",
386
+ sepCostLossAnnually: "Perdita economica annua",
387
+ sepCostLossSpouse: "Impatto stimato su {spouse}",
388
+ sepCostInlineHint: "Duplicazione mensile stimata: {amount}",
389
+ sepCostWarning: "Inserisci le spese mensili in convivenza nel campo sopra per attivare questa analisi.",
370
390
  footerVisitorsTotal: "Visitatori totali",
371
391
  footerVisitorsActive: "Visitatori attivi",
372
392
  footerLoggedUsers: "Utenti loggati",
@@ -455,6 +475,11 @@ const defaultExpenseItems = [
455
475
  authUserFallback: "user",
456
476
  authLogoutDone: "Logout completed.",
457
477
  authLoginRequired: "Please login first.",
478
+ gateNeedsAuth: "This feature is for registered users only. Log in or sign up to continue.",
479
+ gateNeedsAuthTitle: "Login required",
480
+ gateNeedsDonor: "Premium feature for donors. Support the project to unlock it.",
481
+ gateNeedsDonorTitle: "Premium feature",
482
+ gateDonorBadge: "Premium",
458
483
  authSaveFailed: "Cloud profile save failed: {message}",
459
484
  authCloudSaved: "Cloud profile saved. History versions: {count}.",
460
485
  authLoadFailed: "Cloud profile load failed: {message}",
@@ -688,6 +713,19 @@ const defaultExpenseItems = [
688
713
  spiegDetailPerm: "Direct share depends on permanence days with each spouse: direct share = children needs x permanence%.",
689
714
  spiegDetailResultTransfer: "Suggested support comes from the balance: payer theoretical share - payer direct share. If the balance is positive, it is transferred to the other spouse.",
690
715
  spiegDetailResultNoTransfer: "If no balance is positive, the model suggests no transfer between spouses.",
716
+ sepCostBoxTitle: "💸 Cost of separation",
717
+ sepCostBoxNote: "Estimate the monthly expenses the couple would have incurred if living together: the model will compute the duplication cost generated by the separation and the net economic loss per spouse.",
718
+ lblSpeseConvivenza: "Estimated monthly expenses when cohabiting ({currency})",
719
+ hintSpeseConvivenza: "Estimated total monthly expenses of the couple if they had not separated: single rent/mortgage, single utilities, single car, etc. Leave 0 to skip this analysis.",
720
+ sepCostDuplication: "Monthly duplication cost",
721
+ sepCostPanelTitle: "💔 Economic effect of separation",
722
+ sepCostNetTogether: "Combined net if together",
723
+ sepCostNetSeparated: "Combined net after separation",
724
+ sepCostLossMonthly: "Monthly economic loss",
725
+ sepCostLossAnnually: "Annual economic loss",
726
+ sepCostLossSpouse: "Estimated impact on {spouse}",
727
+ sepCostInlineHint: "Estimated monthly duplication: {amount}",
728
+ sepCostWarning: "Enter the cohabiting monthly expenses above to activate this analysis.",
691
729
  footerVisitorsTotal: "Total visitors",
692
730
  footerVisitorsActive: "Active visitors",
693
731
  footerLoggedUsers: "Logged users",
@@ -710,7 +748,8 @@ const defaultExpenseItems = [
710
748
  const authSession = {
711
749
  username: null,
712
750
  userId: null,
713
- keyBits: null
751
+ keyBits: null,
752
+ isDonor: false
714
753
  };
715
754
  const authUiState = {
716
755
  mode: "login",
@@ -1171,6 +1210,14 @@ const defaultExpenseItems = [
1171
1210
  if (permLegendC2) permLegendC2.textContent = c2n();
1172
1211
  if (extraBoxTitle) extraBoxTitle.textContent = tr("extraBoxTitle");
1173
1212
  if (extraBoxNote) extraBoxNote.textContent = tr("extraBoxNote");
1213
+ const sepCostBoxTitleEl = document.getElementById("sepCostBoxTitle");
1214
+ const sepCostBoxNoteEl = document.getElementById("sepCostBoxNote");
1215
+ const lblSpeseConvivenzaEl = document.getElementById("lblSpeseConvivenza");
1216
+ const hintSpeseConvivenzaEl = document.getElementById("hintSpeseConvivenza");
1217
+ if (sepCostBoxTitleEl) sepCostBoxTitleEl.textContent = tr("sepCostBoxTitle");
1218
+ if (sepCostBoxNoteEl) sepCostBoxNoteEl.textContent = tr("sepCostBoxNote");
1219
+ if (lblSpeseConvivenzaEl) lblSpeseConvivenzaEl.textContent = msg("lblSpeseConvivenza", { currency: currentCurrency });
1220
+ if (hintSpeseConvivenzaEl) hintSpeseConvivenzaEl.title = tr("hintSpeseConvivenza");
1174
1221
  if (firstHomeBoxTitle) firstHomeBoxTitle.textContent = tr("firstHomeBoxTitle");
1175
1222
  if (firstHomeBoxNote) firstHomeBoxNote.textContent = tr("firstHomeBoxNote");
1176
1223
  if (lblPrimaCasaMutuoEnabled) lblPrimaCasaMutuoEnabled.textContent = tr("firstHomeMortgageEnabledLabel");
@@ -1642,6 +1689,7 @@ const defaultExpenseItems = [
1642
1689
  authSession.username = username;
1643
1690
  authSession.userId = user.id;
1644
1691
  authSession.keyBits = await deriveSessionKeyBits(password, user.id);
1692
+ authSession.isDonor = localStorage.getItem(`m_donor_${user.id}`) === "1";
1645
1693
  updateAuthUi();
1646
1694
  return msg("authLoginAs", { username });
1647
1695
  }
@@ -1766,6 +1814,55 @@ const defaultExpenseItems = [
1766
1814
  el.textContent = message;
1767
1815
  }
1768
1816
 
1817
+ function isLoggedIn() { return !!authSession.username; }
1818
+ function isDonationPolicyBypassedUser() {
1819
+ return normalizeUsername(authSession.username) === "favagit";
1820
+ }
1821
+ function isDonorUser() {
1822
+ return !!authSession.isDonor || isDonationPolicyBypassedUser();
1823
+ }
1824
+
1825
+ function getScenarioMaxForUser() {
1826
+ if (!isLoggedIn()) return 0;
1827
+ return isDonorUser() ? SCENARIO_LAB_MAX_DONOR : SCENARIO_LAB_MAX_FREE;
1828
+ }
1829
+
1830
+ function showAuthGateMessage(triggerEl) {
1831
+ setAuthMenuOpen(true);
1832
+ setAuthStatus(tr("gateNeedsAuth"), true);
1833
+ if (triggerEl) {
1834
+ triggerEl.classList.add("gate-flash");
1835
+ setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
1836
+ }
1837
+ }
1838
+
1839
+ function showDonorGateMessage(triggerEl) {
1840
+ setAuthMenuOpen(true);
1841
+ setAuthStatus(tr("gateNeedsDonor"), true);
1842
+ if (triggerEl) {
1843
+ triggerEl.classList.add("gate-flash");
1844
+ setTimeout(() => triggerEl.classList.remove("gate-flash"), 700);
1845
+ }
1846
+ const donateBanner = document.querySelector(".donate-banner");
1847
+ if (donateBanner) setTimeout(() => donateBanner.scrollIntoView({ behavior: "smooth", block: "center" }), 200);
1848
+ }
1849
+
1850
+ function applyFeatureGates() {
1851
+ const logged = isLoggedIn();
1852
+ const donor = isDonorUser();
1853
+ const authGatedIds = ["btnPdf", "btnExportJson", "btnImportJson", "btnSaveScenario"];
1854
+ authGatedIds.forEach((id) => {
1855
+ const btn = document.getElementById(id);
1856
+ if (!btn) return;
1857
+ btn.classList.toggle("gate-locked", !logged);
1858
+ btn.classList.toggle("gate-donor", logged && !donor && id === "btnSaveScenario" && false); // donor gate placeholder
1859
+ });
1860
+ const authMenuBtn = document.getElementById("btnAuthMenu");
1861
+ if (authMenuBtn) {
1862
+ authMenuBtn.classList.toggle("has-donor-badge", logged && donor);
1863
+ }
1864
+ }
1865
+
1769
1866
  function updateAuthModeUi() {
1770
1867
  const isSignup = authUiState.mode === "signup";
1771
1868
  const loginModeBtn = document.getElementById("btnAuthModeLogin");
@@ -1809,7 +1906,8 @@ const defaultExpenseItems = [
1809
1906
  if (sessionActions) sessionActions.classList.toggle("is-hidden", !logged);
1810
1907
  if (toggleBtn) {
1811
1908
  toggleBtn.classList.toggle("logged", logged);
1812
- toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}` : tr("authLogin");
1909
+ const badge = logged && authSession.isDonor ? ` ✦` : "";
1910
+ toggleBtn.querySelector("span").textContent = logged ? `${tr("authUserPrefix")}: ${authSession.username}${badge}` : tr("authLogin");
1813
1911
  }
1814
1912
 
1815
1913
  if (logged) {
@@ -1833,6 +1931,7 @@ const defaultExpenseItems = [
1833
1931
  setAuthStatus(tr("authNotAuthenticated"), false);
1834
1932
  }
1835
1933
 
1934
+ applyFeatureGates();
1836
1935
  void syncPresenceTrackState();
1837
1936
  }
1838
1937
 
@@ -2377,6 +2476,7 @@ const defaultExpenseItems = [
2377
2476
  authSession.username = null;
2378
2477
  authSession.userId = null;
2379
2478
  authSession.keyBits = null;
2479
+ authSession.isDonor = false;
2380
2480
  cloudProfileSession.loaded = null;
2381
2481
  cloudProfileSession.history = [];
2382
2482
  updateAuthUi();
@@ -3202,7 +3302,8 @@ const defaultExpenseItems = [
3202
3302
  straordAnn1: num("straordAnn1"),
3203
3303
  straordAnn2: num("straordAnn2"),
3204
3304
  c1SpeseDetails,
3205
- c2SpeseDetails,
3305
+ c2SpeseDetails,
3306
+ speseConvivenza: num("speseConvivenza"),
3206
3307
  c1SpeseDetailUi,
3207
3308
  c2SpeseDetailUi,
3208
3309
  c1Spese,
@@ -3323,7 +3424,18 @@ const defaultExpenseItems = [
3323
3424
  const post1 = disp1 - assegnoDa1a2 + assegnoDa2a1;
3324
3425
  const post2 = disp2 - assegnoDa2a1 + assegnoDa1a2;
3325
3426
 
3326
- return {
3427
+ // Separation cost analysis (only active when speseConvivenza > 0)
3428
+ const speseConvivenza = Math.max(0, Number(payload.speseConvivenza || 0));
3429
+ const costoSeparazioneMensile = speseConvivenza > 0 ? speseTot - speseConvivenza : null;
3430
+ const nettoInsiemeCombinato = speseConvivenza > 0 ? (r1 + r2 - speseConvivenza) : null;
3431
+ const nettoSeparatoTotale = post1 + post2;
3432
+ const perditaMensile = nettoInsiemeCombinato !== null ? nettoInsiemeCombinato - nettoSeparatoTotale : null;
3433
+ const perditaAnnua = perditaMensile !== null ? perditaMensile * 12 : null;
3434
+ const totReddito = Math.max(0.001, r1 + r2);
3435
+ const perditaSpouse1 = perditaMensile !== null ? perditaMensile * (r1 / totReddito) : null;
3436
+ const perditaSpouse2 = perditaMensile !== null ? perditaMensile * (r2 / totReddito) : null;
3437
+
3438
+ return {
3327
3439
  r1, r2, r1Raw, r2Raw, incomeMode, figli, perm1, perm2,
3328
3440
  aPerc1, aPag1, aPerc2, aPag2, aFam1, aFam2,
3329
3441
  match12, match21, esternoPag1, esternoPag2,
@@ -3341,7 +3453,11 @@ const defaultExpenseItems = [
3341
3453
  primaCasaConsidered, primaCasaTransfer1to2, primaCasaTransfer2to1,
3342
3454
  compensativeBenefits,
3343
3455
  assegnoDa1a2, assegnoDa2a1,
3344
- post1, post2
3456
+ post1, post2,
3457
+ speseConvivenza, costoSeparazioneMensile,
3458
+ nettoInsiemeCombinato, nettoSeparatoTotale,
3459
+ perditaMensile, perditaAnnua,
3460
+ perditaSpouse1, perditaSpouse2
3345
3461
  };
3346
3462
  }
3347
3463
 
@@ -3930,7 +4046,7 @@ const defaultExpenseItems = [
3930
4046
  }
3931
4047
 
3932
4048
  function saveCurrentScenario() {
3933
- if (scenarioLab.length >= SCENARIO_LAB_MAX) {
4049
+ if (scenarioLab.length >= getScenarioMaxForUser()) {
3934
4050
  alert(tr("scenarioLabMaxReached"));
3935
4051
  return;
3936
4052
  }
@@ -4465,6 +4581,57 @@ const defaultExpenseItems = [
4465
4581
  });
4466
4582
  }
4467
4583
 
4584
+ function renderSeparationCostPanel(m) {
4585
+ const panel = document.getElementById("sepCostPanel");
4586
+ if (!panel) return;
4587
+
4588
+ // Update inline hint showing duplication cost
4589
+ const hintDiv = document.getElementById("speseConvivenzaHint");
4590
+ if (hintDiv) {
4591
+ hintDiv.textContent = m.speseConvivenza > 0 && m.costoSeparazioneMensile !== null
4592
+ ? msg("sepCostInlineHint", { amount: eur(m.costoSeparazioneMensile) })
4593
+ : "";
4594
+ }
4595
+
4596
+ if (!m.speseConvivenza || m.speseConvivenza <= 0) {
4597
+ panel.innerHTML = `<div class="sep-cost-warning">${escapeHtml(tr("sepCostWarning"))}</div>`;
4598
+ return;
4599
+ }
4600
+
4601
+ const c1Name = c1n();
4602
+ const c2Name = c2n();
4603
+ const c1NameEsc = escapeHtml(c1Name);
4604
+ const c2NameEsc = escapeHtml(c2Name);
4605
+
4606
+ const lossClass = (v) => v === null ? "" : v > 0 ? "sep-loss-negative" : "sep-loss-positive";
4607
+ const rowHtml = (label, value, em) => {
4608
+ const cls = lossClass(value);
4609
+ const formatted = value === null ? "&mdash;" : eur(value);
4610
+ return `<div class="sep-cost-row${em ? " sep-cost-row--em" : ""}">
4611
+ <span class="sep-cost-label">${label}</span>
4612
+ <strong class="sep-cost-value ${cls}">${formatted}</strong>
4613
+ </div>`;
4614
+ };
4615
+
4616
+ panel.innerHTML = `
4617
+ <div class="sep-cost-panel">
4618
+ <h3 class="sep-cost-title">${escapeHtml(tr("sepCostPanelTitle"))}</h3>
4619
+ <div class="sep-cost-section">
4620
+ ${rowHtml(tr("sepCostNetTogether"), m.nettoInsiemeCombinato, false)}
4621
+ ${rowHtml(tr("sepCostNetSeparated"), m.nettoSeparatoTotale, false)}
4622
+ </div>
4623
+ <div class="sep-cost-divider"></div>
4624
+ <div class="sep-cost-section">
4625
+ ${rowHtml(tr("sepCostDuplication"), m.costoSeparazioneMensile, false)}
4626
+ ${rowHtml(tr("sepCostLossMonthly"), m.perditaMensile, true)}
4627
+ ${rowHtml(tr("sepCostLossAnnually"), m.perditaAnnua, true)}
4628
+ ${rowHtml(msg("sepCostLossSpouse", { spouse: c1NameEsc }), m.perditaSpouse1, false)}
4629
+ ${rowHtml(msg("sepCostLossSpouse", { spouse: c2NameEsc }), m.perditaSpouse2, false)}
4630
+ </div>
4631
+ </div>
4632
+ `;
4633
+ }
4634
+
4468
4635
  function renderAll() {
4469
4636
  const m = computeModel();
4470
4637
  updateExtraordinaryModuleUi();
@@ -4472,6 +4639,7 @@ const defaultExpenseItems = [
4472
4639
  renderLivePanel(m);
4473
4640
  calculate(m);
4474
4641
  renderSpiegabilita(m);
4642
+ renderSeparationCostPanel(m);
4475
4643
  renderScenarioLab();
4476
4644
  }
4477
4645
 
@@ -5303,7 +5471,8 @@ ${scenarioLab.length ? `
5303
5471
  primaCasaAssegnataA: String(document.getElementById("primaCasaAssegnataA")?.value || ""),
5304
5472
  primaCasaMutuoPerc1: num("primaCasaMutuoPerc1"),
5305
5473
  straordAnn1: num("straordAnn1"),
5306
- straordAnn2: num("straordAnn2")
5474
+ straordAnn2: num("straordAnn2"),
5475
+ speseConvivenza: num("speseConvivenza")
5307
5476
  };
5308
5477
  const spese = expenseItems.map((_, i) => ({
5309
5478
  c1: num(`c1_${i}`),
@@ -5438,10 +5607,12 @@ ${scenarioLab.length ? `
5438
5607
  document.getElementById("btnReset").addEventListener("click", resetAll);
5439
5608
 
5440
5609
  document.getElementById("btnExportJson").addEventListener("click", async () => {
5610
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnExportJson")); return; }
5441
5611
  await exportJson();
5442
5612
  });
5443
5613
 
5444
5614
  document.getElementById("btnImportJson").addEventListener("click", () => {
5615
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnImportJson")); return; }
5445
5616
  document.getElementById("fileJson").click();
5446
5617
  });
5447
5618
 
@@ -5454,10 +5625,12 @@ ${scenarioLab.length ? `
5454
5625
  });
5455
5626
 
5456
5627
  document.getElementById("btnPdf").addEventListener("click", () => {
5628
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnPdf")); return; }
5457
5629
  exportPdfDirect();
5458
5630
  });
5459
5631
 
5460
5632
  document.getElementById("btnSaveScenario").addEventListener("click", () => {
5633
+ if (!isLoggedIn()) { showAuthGateMessage(document.getElementById("btnSaveScenario")); return; }
5461
5634
  saveCurrentScenario();
5462
5635
  });
5463
5636
 
@@ -414,6 +414,20 @@
414
414
  </div>
415
415
  </div>
416
416
 
417
+ <div class="extra-box extra-box-sep-cost" id="sepCostBox">
418
+ <div class="extra-box-title" id="sepCostBoxTitle">💸 Costo della separazione</div>
419
+ <div class="extra-box-note" id="sepCostBoxNote">Stima le spese mensili che la coppia avrebbe sostenuto vivendo insieme: il modello calcolerà il costo di duplicazione generato dalla separazione e la perdita economica netta per ciascun coniuge.</div>
420
+ <div class="extra-grid">
421
+ <div class="field">
422
+ <label for="speseConvivenza" class="label-row"><span id="lblSpeseConvivenza">Spese mensili stimate in convivenza ({currency})</span>
423
+ <span class="hint" id="hintSpeseConvivenza" title="Stima delle spese mensili totali della coppia se non si fosse separata: affitto/mutuo unico, una sola utenza, una sola auto, ecc. Lascia 0 per non includere questa analisi.">i</span>
424
+ </label>
425
+ <input id="speseConvivenza" type="number" min="0" step="50" value="0" />
426
+ <div class="extra-monthly" id="speseConvivenzaHint" style="color:#c05a00"></div>
427
+ </div>
428
+ </div>
429
+ </div>
430
+
417
431
 
418
432
  <p class="spese-count-title" id="speseCountNote">
419
433
  Elenco spese compilabili.
@@ -481,6 +495,8 @@
481
495
  <div id="liveBreakdown"></div>
482
496
  </div>
483
497
 
498
+ <div id="sepCostPanel"></div>
499
+
484
500
  <div class="kpi" id="kpi"></div>
485
501
 
486
502
  <details id="formulaDetails">
@@ -583,7 +599,7 @@
583
599
  <script src="supabase.min.js"></script>
584
600
  <script src="fabric.min.js"></script>
585
601
  <script src="html2pdf.bundle.min.js"></script>
586
- <script src="app.js?v=2.1.8"></script>
602
+ <script src="app.js?v=2.2.0"></script>
587
603
  </body>
588
604
  </html>
589
605
 
@@ -1388,6 +1388,37 @@
1388
1388
  border: 1px solid #c6cdc3;
1389
1389
  }
1390
1390
 
1391
+ /* ── Feature gate ─────────────────────────────────────────────── */
1392
+ .gate-locked {
1393
+ position: relative;
1394
+ opacity: 0.62;
1395
+ cursor: pointer;
1396
+ }
1397
+
1398
+ .gate-locked::after {
1399
+ content: "\uD83D\uDD12";
1400
+ position: absolute;
1401
+ top: 2px;
1402
+ right: 4px;
1403
+ font-size: 0.65rem;
1404
+ line-height: 1;
1405
+ pointer-events: none;
1406
+ }
1407
+
1408
+ .gate-locked:hover {
1409
+ opacity: 0.85;
1410
+ }
1411
+
1412
+ @keyframes gate-flash-anim {
1413
+ 0% { outline: 2px solid transparent; }
1414
+ 30% { outline: 2px solid #d04a2f; box-shadow: 0 0 0 3px rgba(208,74,47,0.22); }
1415
+ 100% { outline: 2px solid transparent; }
1416
+ }
1417
+
1418
+ .gate-flash {
1419
+ animation: gate-flash-anim 0.65s ease forwards;
1420
+ }
1421
+
1391
1422
  /* ── Scenario Lab ─────────────────────────────────────────────── */
1392
1423
  .scenario-lab-card {
1393
1424
  margin-top: 14px;
@@ -1725,8 +1756,8 @@
1725
1756
 
1726
1757
  .spieg-benefit-card {
1727
1758
  display: grid;
1728
- grid-template-columns: 28px 1fr auto;
1729
- align-items: center;
1759
+ grid-template-columns: 28px minmax(0, 1fr) auto;
1760
+ align-items: start;
1730
1761
  gap: 8px;
1731
1762
  padding: 8px 10px;
1732
1763
  border-radius: 10px;
@@ -1754,6 +1785,8 @@
1754
1785
  font-weight: 600;
1755
1786
  color: #1f4642;
1756
1787
  word-break: break-word;
1788
+ min-width: 0;
1789
+ padding-top: 5px;
1757
1790
  }
1758
1791
 
1759
1792
  .spieg-benefit-amount {
@@ -1788,6 +1821,83 @@
1788
1821
  font-variant-numeric: tabular-nums;
1789
1822
  }
1790
1823
 
1824
+ .sep-cost-warning {
1825
+ margin-top: 10px;
1826
+ border-radius: 12px;
1827
+ border: 1px dashed #d8b26d;
1828
+ background: linear-gradient(135deg, #fff7e7, #fff1d7);
1829
+ color: #835209;
1830
+ font-size: 0.86rem;
1831
+ font-weight: 700;
1832
+ padding: 10px 12px;
1833
+ }
1834
+
1835
+ .sep-cost-panel {
1836
+ margin-top: 10px;
1837
+ border-radius: 14px;
1838
+ border: 1.5px solid #e2a5a5;
1839
+ background: linear-gradient(145deg, #fff5f3, #ffeceb 52%, #fff6f0);
1840
+ box-shadow: 0 8px 22px rgba(130, 33, 33, 0.12);
1841
+ padding: 12px;
1842
+ }
1843
+
1844
+ .sep-cost-title {
1845
+ margin: 0 0 8px;
1846
+ font-size: 0.98rem;
1847
+ font-weight: 900;
1848
+ color: #842d2d;
1849
+ letter-spacing: 0.2px;
1850
+ }
1851
+
1852
+ .sep-cost-section {
1853
+ display: grid;
1854
+ gap: 7px;
1855
+ }
1856
+
1857
+ .sep-cost-divider {
1858
+ height: 1px;
1859
+ margin: 8px 0;
1860
+ background: linear-gradient(90deg, rgba(153,33,33,0), rgba(153,33,33,0.28), rgba(153,33,33,0));
1861
+ }
1862
+
1863
+ .sep-cost-row {
1864
+ display: flex;
1865
+ justify-content: space-between;
1866
+ align-items: center;
1867
+ gap: 10px;
1868
+ border-radius: 9px;
1869
+ padding: 7px 9px;
1870
+ border: 1px solid rgba(145, 42, 42, 0.13);
1871
+ background: rgba(255, 255, 255, 0.72);
1872
+ }
1873
+
1874
+ .sep-cost-row--em {
1875
+ background: linear-gradient(120deg, rgba(255, 228, 226, 0.86), rgba(255, 241, 225, 0.88));
1876
+ border-color: rgba(180, 72, 43, 0.32);
1877
+ }
1878
+
1879
+ .sep-cost-label {
1880
+ font-size: 0.82rem;
1881
+ color: #6a3939;
1882
+ font-weight: 700;
1883
+ }
1884
+
1885
+ .sep-cost-value {
1886
+ font-size: 0.94rem;
1887
+ font-weight: 900;
1888
+ font-variant-numeric: tabular-nums;
1889
+ white-space: nowrap;
1890
+ color: #5d2e2e;
1891
+ }
1892
+
1893
+ .sep-loss-negative {
1894
+ color: #9b1f1f;
1895
+ }
1896
+
1897
+ .sep-loss-positive {
1898
+ color: #0d7a57;
1899
+ }
1900
+
1791
1901
  @page {
1792
1902
  size: A4 landscape;
1793
1903
  margin: 8mm;
@@ -2209,7 +2319,6 @@
2209
2319
  box-shadow: 0 12px 26px rgba(17, 70, 64, 0.16);
2210
2320
  margin-bottom: 10px;
2211
2321
  position: relative;
2212
- overflow: hidden;
2213
2322
  }
2214
2323
 
2215
2324
  .result-pill::after {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mantenimento-app",
3
- "version": "2.1.8",
3
+ "version": "2.2.0",
4
4
  "description": "Frontend + backend architecture for the mantenimento calculator",
5
5
  "type": "commonjs",
6
6
  "main": "backend/calculate-model.js",