fix(planner): mobile polish — z-index fixes, sidebar alignment, table layout, German i18n

- Fix quote sidebar z-index (behind tab nav) and align top with tab content
- Fix bottom nav sticky positioning (move outside .planner-app)
- Fix wizard footer fixed positioning and width on mobile
- Fix bottom nav active state visibility (hardcoded colors outside CSS var scope)
- Fix country pills overflow with flex-wrap
- Fix tooltip clipping in collapsible sections
- Hide feedback button on mobile planner
- Add cache busting for static assets (_ASSET_VERSION)
- Convert export CTA to full clickable button
- Add CAPEX table section header, sort doughnut chart by size
- Cap data tables at 640px centered, horizontal scroll for wide tables
- Replace CAPEX jargon with plain German (Gesamtinvestition, Kostenaufschlüsselung)
- Update FAQ/landing copy to global language (not Europe-specific)
- Update default court sizes to realistic values (court + walkway only)
- Add missing planner_export_inline translation key (en + de)
- Revert wizard nav to client-side (HTMX broke on lang-prefixed routes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-22 17:07:31 +01:00
parent 37caf3db66
commit a36aef7116
11 changed files with 141 additions and 97 deletions

View File

@@ -1,6 +1,7 @@
"""
Padelnomics - Application factory and entry point.
"""
import time
from pathlib import Path
from quart import Quart, Response, abort, g, redirect, request, session, url_for
@@ -9,6 +10,8 @@ from .analytics import close_analytics_db, open_analytics_db
from .core import close_db, config, get_csrf_token, init_db, setup_request_id
from .i18n import LANG_BLUEPRINTS, SUPPORTED_LANGS, get_translations
_ASSET_VERSION = str(int(time.time()))
def _detect_lang() -> str:
"""Detect preferred language from cookie then Accept-Language header."""
@@ -220,6 +223,7 @@ def create_app() -> Quart:
"ab_tag": getattr(g, "ab_tag", None),
"lang": effective_lang,
"t": get_translations(effective_lang),
"v": _ASSET_VERSION,
}
# -------------------------------------------------------------------------

View File

@@ -224,6 +224,7 @@
"planner_quote_cta_check_4": "Nur passende Anbieter sehen deine Anfrage",
"planner_quote_cta_btn": "Angebote einholen →",
"planner_quote_cta_hint": "Dauert ca. 2 Minuten",
"planner_export_inline": "Teile diese Analyse mit Partnern oder Investoren",
"planner_export_btn": "Geschäftsplan exportieren (PDF) →",
"planner_export_hint": "99 € einmalig · Bankfertig",
"planner_signup_bar_msg": "Erstelle ein Konto, um Szenarien zu speichern und Pläne zu vergleichen.",
@@ -802,12 +803,12 @@
"card_total_courts": "Plätze gesamt",
"card_floor_area": "Grundfläche",
"card_court_area": "Platzfläche",
"card_total_capex": "Gesamt-CAPEX",
"card_total_capex": "Gesamtinvestition",
"card_per_court": "Pro Platz",
"card_per_sqm": "Pro m²",
"budget_over": "BUDGET ÜBERSCHRITTEN",
"budget_under": "IM BUDGET",
"table_total_capex": "GESAMT-CAPEX",
"table_total_capex": "GESAMTINVESTITION",
"th_item": "Position",
"th_amount": "Betrag",
"card_net_rev_mo": "Nettoumsatz/Monat",
@@ -1109,14 +1110,14 @@
"planner_section_util": "Auslastung & Betrieb",
"planner_step3_title": "Investition & Baukosten",
"planner_step3_sub": "Konfiguriere Baukosten, Glas- und Beleuchtungsoptionen sowie Dein Budgetziel.",
"planner_section_capex": "Bau & CAPEX",
"planner_section_capex": "Investitionskosten (CAPEX)",
"planner_hint_adjust": "Nach Szenario anpassen",
"planner_step4_title": "Betrieb & Finanzierung",
"planner_step4_sub": "Monatliche Betriebskosten, Kreditkonditionen und Exit-Annahmen.",
"planner_section_opex": "Monatliche Betriebskosten",
"planner_section_financing": "Finanzierung",
"planner_section_exit": "Exit-Annahmen",
"planner_chart_capex": "CAPEX-Aufschlüsselung",
"planner_chart_capex": "Kostenaufschlüsselung",
"planner_chart_cf": "Monatlicher Netto-Cashflow (60 Monate)",
"planner_chart_cum": "Kumulierter Cashflow",
"planner_section_annual": "Jahresübersicht",
@@ -1151,7 +1152,7 @@
"about_meta_desc": "Padelnomics ist eine kostenlose Finanzplanungsplattform für Padel-Unternehmer. Modelliere deine Investition, finde Anbieter und plane dein Padel-Business mit professionellen Tools.",
"about_og_desc": "Entwickelt für Padel-Unternehmer, die professionelle Finanztools ohne Beratungskosten benötigen. Kostenloser Planer, 60+ Variablen, Anbieterverzeichnis und mehr.",
"about_h1": "Über Padelnomics",
"about_body_p1": "Padel ist der am schnellsten wachsende Sport in Europa, doch für die meisten Unternehmer ist die Eröffnung einer Paddelhalle immer noch ein Sprung ins Ungewisse. Die Finanzen sind komplex: Der CAPEX variiert stark je nach Anlagentyp, der Standort bestimmt die Auslastung, und der Unterschied zwischen 60 % und 75 % Belegung kann über Erfolg oder Misserfolg einer Investition entscheiden.",
"about_body_p1": "Padel ist eine der am schnellsten wachsenden Sportarten weltweit, doch für die meisten Unternehmer ist die Eröffnung einer Paddelhalle immer noch ein Sprung ins Ungewisse. Die Finanzen sind komplex: Der CAPEX variiert stark je nach Anlagentyp, der Standort bestimmt die Auslastung, und der Unterschied zwischen 60 % und 75 % Belegung kann über Erfolg oder Misserfolg einer Investition entscheiden.",
"about_body_p2": "Wir haben Padelnomics gebaut, weil wir kein ausreichend gutes Finanzplanungstool gefunden haben. Vorhandene Rechner sind entweder zu simpel (5 Eingaben, ein Ergebnis) oder hinter teuren Beratungsmandaten verborgen. Wir wollten etwas mit der Tiefe eines professionellen Finanzmodells, aber der Zugänglichkeit einer Web-App.",
"about_body_p3": "Das Ergebnis ist ein kostenloser Finanzplaner mit 60+ anpassbaren Variablen, 6 Analyse-Tabs, Sensitivitätsanalyse und den professionellen Kennzahlen, die Banken und Investoren sehen müssen. Jede Annahme ist transparent und anpassbar. Keine Blackboxen.",
"about_why_p": "Der Planer ist kostenlos, weil wir glauben, dass bessere Planung zu besseren Padelanlagen führt — und das ist gut für die gesamte Branche. Wir verdienen Geld, indem wir Unternehmer mit Platz-Anbietern und Finanzierungspartnern verbinden, wenn sie bereit sind, von der Planung zum Bau überzugehen.",
@@ -1191,8 +1192,8 @@
"landing_faq_a2": "Nein. Der Planer funktioniert sofort ohne Registrierung. Erstelle ein Konto, um Szenarien zu speichern, Konfigurationen zu vergleichen und PDF-Berichte zu exportieren.",
"landing_faq_a3": "Wenn du über den Planer Angebote anforderst, teilen wir deine Projektdetails (Anlagentyp, Platzzahl, Glas, Beleuchtung, Land, Budget, Zeitplan) mit passenden Anbietern aus unserem Verzeichnis. Diese kontaktieren dich direkt mit ihren Angeboten.",
"landing_faq_a4": "Das Durchsuchen des Verzeichnisses ist für alle kostenlos. Anbieter erhalten standardmäßig einen Basiseintrag. Kostenpflichtige Pläne (Basic ab 39 €/Monat, Growth ab 199 €/Monat, Pro ab 499 €/Monat) schalten Anfrageformulare, vollständige Beschreibungen, Logos, verifizierte Badges und Prioritätsplatzierung frei.",
"landing_faq_a5": "Das Modell verwendet reale Standardwerte auf Basis europäischer Marktdaten. Jede Annahme ist anpassbar, sodass du deine lokalen Gegebenheiten abbilden kannst. Die Sensitivitätsanalyse zeigt, wie sich die Ergebnisse in verschiedenen Szenarien verändern, und hilft dir, die Bandbreite möglicher Ergebnisse zu verstehen.",
"landing_seo_p1": "Padel ist der am schnellsten wachsende Sport in Europa — die Nachfrage nach Plätzen übersteigt das Angebot in Deutschland, Österreich, der Schweiz und darüber hinaus bei weitem. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Indoorhalle mit 68 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 23 Mio. € (Neubau), mit Amortisationszeiten von 35 Jahren für gut gelegene Anlagen.",
"landing_faq_a5": "Das Modell verwendet reale Standardwerte auf Basis globaler Marktdaten. Jede Annahme ist anpassbar, sodass du deine lokalen Gegebenheiten abbilden kannst. Die Sensitivitätsanalyse zeigt, wie sich die Ergebnisse in verschiedenen Szenarien verändern, und hilft dir, die Bandbreite möglicher Ergebnisse zu verstehen.",
"landing_seo_p1": "Padel ist eine der am schnellsten wachsenden Racketsportarten weltweit — die Nachfrage nach Plätzen übersteigt das Angebot von Deutschland, Spanien und Schweden bis in die USA und den Nahen Osten. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Indoorhalle mit 68 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 23 Mio. € (Neubau), mit Amortisationszeiten von 35 Jahren für gut gelegene Anlagen.",
"landing_seo_p2": "Die entscheidenden Faktoren für den Erfolg sind Standort (treibt die Auslastung), Baukosten (CAPEX), Miet- oder Grundstückskosten sowie die Preisstrategie. Unser Finanzplaner ermöglicht es dir, alle diese Variablen interaktiv zu modellieren und die Auswirkungen auf IRR, MOIC, Cashflow und Schuldendienstdeckungsgrad in Echtzeit zu sehen. Ob du als Unternehmer deine erste Anlage prüfst, als Immobilienentwickler Padel in ein Mixed-Use-Projekt integrierst oder als Investor eine bestehende Paddelhalle bewertest — Padelnomics gibt dir die finanzielle Klarheit für fundierte Entscheidungen.",
"landing_final_cta_sub": "Modelliere deine Investition und lass dich mit verifizierten Platz-Anbietern aus {total_countries} Ländern zusammenbringen.",
"landing_jsonld_org_desc": "Professionelle Planungsplattform für Padelplatz-Investitionen. Finanzplaner, Anbieterverzeichnis und Marktinformationen für Padel-Unternehmer.",

View File

@@ -224,6 +224,7 @@
"planner_quote_cta_check_4": "Only matching suppliers see your request",
"planner_quote_cta_btn": "Get Quotes →",
"planner_quote_cta_hint": "Takes ~2 minutes",
"planner_export_inline": "Share this analysis with partners or investors",
"planner_export_btn": "Export Business Plan (PDF) →",
"planner_export_hint": "€99 one-time · Bank-ready",
"planner_signup_bar_msg": "Create an account to save scenarios and compare plans.",
@@ -1151,7 +1152,7 @@
"about_meta_desc": "Padelnomics is a free financial planning platform for padel entrepreneurs. Model your investment, find suppliers, and plan your padel court business with professional-grade tools.",
"about_og_desc": "Built for padel entrepreneurs who need professional financial tools without consulting fees. Free planner, 60+ variables, supplier directory, and more.",
"about_h1": "About Padelnomics",
"about_body_p1": "Padel is the fastest-growing sport in Europe, but opening a padel hall is still a leap of faith for most entrepreneurs. The financials are complex: CAPEX varies wildly depending on venue type, location drives utilization, and the difference between a 60% and 75% occupancy rate can mean the difference between a great investment and a money pit.",
"about_body_p1": "Padel is one of the fastest-growing sports worldwide, but opening a padel hall is still a leap of faith for most entrepreneurs. The financials are complex: CAPEX varies wildly depending on venue type, location drives utilization, and the difference between a 60% and 75% occupancy rate can mean the difference between a great investment and a money pit.",
"about_body_p2": "We built Padelnomics because we couldn't find a financial planning tool that was good enough. Existing calculators are either too simplistic (5 inputs, one output) or locked behind expensive consulting engagements. We wanted something with the depth of a professional financial model but the accessibility of a web app.",
"about_body_p3": "The result is a free financial planner with 60+ adjustable variables, 6 analysis tabs, sensitivity analysis, and the professional metrics that banks and investors need to see. Every assumption is transparent and adjustable. No black boxes.",
"about_why_p": "The planner is free because we believe better planning leads to better padel venues, and that's good for the entire industry. We make money by connecting entrepreneurs with court suppliers and financing partners when they're ready to move from planning to building.",
@@ -1191,8 +1192,8 @@
"landing_faq_a2": "No. The planner works instantly with no signup. Create an account to save scenarios, compare configurations, and export PDF reports.",
"landing_faq_a3": "When you request quotes through the planner, we share your project details (venue type, court count, glass, lighting, country, budget, timeline) with relevant suppliers from our directory. They contact you directly with proposals.",
"landing_faq_a4": "Browsing the directory is free for everyone. Suppliers have a basic listing by default. Paid plans (Basic at €39/mo, Growth at €199/mo, Pro at €499/mo) unlock enquiry forms, full descriptions, logos, verified badges, and priority placement.",
"landing_faq_a5": "The model uses real-world defaults based on European market data. Every assumption is adjustable so you can match your local conditions. The sensitivity analysis shows how results change across different scenarios, helping you understand the range of outcomes.",
"landing_seo_p1": "Padel is the fastest-growing sport in Europe, with demand for courts far outstripping supply in Germany, the UK, Scandinavia, and beyond. Opening a padel hall can be a lucrative investment, but the numbers need to work. A typical indoor padel venue with 6-8 courts requires between €300K (renting an existing building) and €2-3M (building new), with payback periods of 3-5 years for well-located venues.",
"landing_faq_a5": "The model uses real-world defaults based on global market data. Every assumption is adjustable so you can match your local conditions. The sensitivity analysis shows how results change across different scenarios, helping you understand the range of outcomes.",
"landing_seo_p1": "Padel is one of the fastest-growing racket sports globally, with demand for courts outstripping supply across markets from Germany, Spain, and Sweden to the US and Middle East. Opening a padel hall can be a lucrative investment, but the numbers need to work. A typical indoor padel venue with 6-8 courts requires between €300K (renting an existing building) and €2-3M (building new), with payback periods of 3-5 years for well-located venues.",
"landing_seo_p2": "The key variables that determine success are location (driving utilization), construction costs (CAPEX), rent or land costs, and pricing strategy. Our financial planner lets you model all of these variables interactively, seeing the impact on your IRR, MOIC, cash flow, and debt service coverage ratio in real time. Whether you're an entrepreneur exploring your first venue, a real estate developer adding padel to a mixed-use project, or an investor evaluating a padel hall acquisition, Padelnomics gives you the financial clarity to make informed decisions.",
"landing_final_cta_sub": "Model your investment, then get matched with verified court suppliers across {total_countries} countries.",
"landing_jsonld_org_desc": "Professional padel court investment planning platform. Financial planner, supplier directory, and market intelligence for padel entrepreneurs.",

View File

@@ -20,11 +20,11 @@ DEFAULTS = {
"venue": "indoor",
"own": "rent",
"dblCourts": 4,
"sglCourts": 2,
"sqmPerDblHall": 336,
"sqmPerSglHall": 240,
"sqmPerDblOutdoor": 312,
"sqmPerSglOutdoor": 216,
"sglCourts": 0,
"sqmPerDblHall": 250,
"sqmPerSglHall": 160,
"sqmPerDblOutdoor": 230,
"sqmPerSglOutdoor": 150,
"ratePeak": 50,
"rateOffPeak": 35,
"rateSingle": 30,

View File

@@ -99,7 +99,11 @@ def augment_d(d: dict, s: dict, lang: str) -> None:
"#EC4899", "#06B6D4", "#84CC16", "#F97316", "#475569",
"#0EA5E9", "#A78BFA",
]
_cap_items = [i for i in d["capexItems"] if i["amount"] > 0]
_cap_items = sorted(
[i for i in d["capexItems"] if i["amount"] > 0],
key=lambda i: i["amount"],
reverse=True,
)
d["capex_chart"] = {
"type": "doughnut",
"data": {
@@ -114,7 +118,7 @@ def augment_d(d: dict, s: dict, lang: str) -> None:
"responsive": True,
"maintainAspectRatio": False,
"cutout": "60%",
"plugins": {"legend": {"position": "right", "labels": {"boxWidth": 10, "font": {"size": 10}}}},
"plugins": {"legend": {"position": "right", "labels": {"boxWidth": 10, "font": {"size": 12}, "padding": 8}}},
},
}
@@ -380,18 +384,6 @@ async def calculate():
TOTAL_WIZARD_STEPS = 4
@bp.route("/wizard-nav")
async def wizard_nav():
"""HTMX endpoint: render wizard Back/Next/Calculate buttons for a given step."""
step = request.args.get("step", 1, type=int)
step = max(1, min(step, TOTAL_WIZARD_STEPS))
lang = g.get("lang", "en")
t = get_translations(lang)
return await render_template(
"partials/wizard_nav.html", step=step, total_steps=TOTAL_WIZARD_STEPS, t=t,
)
@bp.route("/scenarios", methods=["GET"])
@login_required
async def scenario_list():

View File

@@ -22,6 +22,8 @@
{% endif %}
</div>
<div class="mb-section">
<div class="section-header"><h3>{{ t.planner_section_capex }}</h3></div>
<table class="data-table">
<thead>
<tr><th>{{ t.th_item }}</th><th class="right">{{ t.th_amount }}</th></tr>
@@ -39,6 +41,7 @@
</tr>
</tbody>
</table>
</div>
<div class="chart-container mt-4">
<div class="chart-container__label">{{ t.planner_chart_capex }}</div>

View File

@@ -108,7 +108,7 @@
</div>
</details>
<div class="export-cta-inline">
<span>{{ t.planner_export_inline|default('Share this analysis with partners or investors') }}</span>
<a href="{{ url_for('planner.export') }}">{{ t.planner_export_btn }} &rarr;</a>
</div>
<a href="{{ url_for('planner.export') }}" class="export-cta-inline">
<span>{{ t.planner_export_inline }}</span>
<span class="export-cta-inline__btn">{{ t.planner_export_btn }}</span>
</a>

View File

@@ -7,7 +7,7 @@
<meta property="og:description" content="{{ t.planner_meta_desc }}">
<meta property="og:type" content="website">
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/planner.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/planner.css') }}?v={{ v }}">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
{% endblock %}
@@ -345,7 +345,8 @@
<!-- Wizard footer: preview bar + navigation -->
<div class="wizard-footer">
<div class="wizard-preview" id="wizPreview">{% include "partials/wizard_preview.html" %}</div>
<div class="wizard-nav" id="wizNav">
<div class="wizard-nav" id="wizNav"
data-back="{{ t.btn_back }}" data-next="{{ t.btn_next }}" data-results="{{ t.btn_show_results }}">
{% include "partials/wizard_nav.html" %}
</div>
</div>
@@ -392,7 +393,9 @@
<div id="scenario-drawer"></div>
<div id="save-feedback"></div>
<!-- Mobile bottom tab bar -->
</div>
<!-- Mobile bottom tab bar (outside .planner-app to avoid ancestor interference with position:fixed) -->
<nav class="bottom-nav" id="bottomNav">
<button class="bottom-nav__btn bottom-nav__btn--active" data-tab="assumptions" onclick="setActiveTab('assumptions')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
@@ -423,7 +426,6 @@
<span>{{ t.tab_returns_short|default('Returns') }}</span>
</button>
</nav>
</div>
{% endblock %}
{% block scripts %}
@@ -431,5 +433,5 @@
window.__COUNTRY_PRESETS__ = {{ country_presets | tojson | safe }};
window.__QUOTE_URL__ = "{{ url_for('leads.quote_request') }}";
</script>
<script src="{{ url_for('static', filename='js/planner.js') }}"></script>
<script src="{{ url_for('static', filename='js/planner.js') }}?v={{ v }}"></script>
{% endblock %}

View File

@@ -365,10 +365,10 @@
}
.pill-options {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.pill-btn {
flex: 1;
padding: 6px 10px;
font-size: 11px;
font-weight: 600;
@@ -409,6 +409,8 @@
/* ── Data Tables ── */
.data-table {
width: 100%;
max-width: 640px;
margin: 0 auto;
border-collapse: collapse;
font-size: 12px;
}
@@ -531,7 +533,7 @@
}
/* ── Spacing helpers ── */
.mb-section { margin-bottom: 28px; }
.mb-section { margin-bottom: 28px; max-width: 640px; margin-left: auto; margin-right: auto; overflow-x: auto; }
.mb-4 { margin-bottom: 1rem; }
.mt-4 { margin-top: 1rem; }
@@ -627,14 +629,14 @@
display: block;
position: fixed;
right: max(1rem, calc((100vw - 72rem) / 2 - 280px));
top: 80px;
top: calc(50px + 1.5rem);
width: 240px;
background: #EFF6FF;
border: 2px solid #1D4ED8;
border-radius: 20px;
padding: 24px;
box-shadow: 0 4px 24px rgba(29,78,216,0.1);
z-index: 40;
z-index: 60;
}
.quote-sidebar__label {
font-size: 11px;
@@ -1134,30 +1136,53 @@
.wizard-step { max-width: 100%; }
.wizard-preview,
.wizard-nav { max-width: 100%; }
.wizard-step { padding-bottom: 100px; } /* space for sticky footer */
.wizard-step { padding-bottom: 140px; } /* space for fixed footer + bottom nav */
.wizard-footer {
position: fixed;
bottom: 56px; /* sit directly above bottom nav */
left: 0;
right: 0;
z-index: 55;
margin-top: 0;
padding: 0 1.5rem;
background: #FFFFFF;
border-top: 1px solid #E2E8F0;
}
.wizard-nav {
max-width: 560px;
margin: 0 auto;
border-radius: 0;
border: none;
background: transparent;
}
}
/* ── Export inline CTA (within Returns tab) ── */
.export-cta-inline {
margin-top: 1.5rem;
padding: 14px 16px;
background: var(--gn-bg);
border: 1px solid rgba(22,163,74,0.2);
border-radius: 12px;
font-size: 12px;
color: var(--txt-2);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.export-cta-inline a {
color: var(--gn);
font-weight: 600;
margin-top: 1.5rem;
padding: 14px 20px;
background: var(--gn-bg);
border: 1px solid rgba(22,163,74,0.2);
border-radius: 12px;
font-size: 13px;
color: var(--txt-2);
text-decoration: none;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.export-cta-inline:hover {
background: rgba(22,163,74,0.1);
border-color: rgba(22,163,74,0.35);
}
.export-cta-inline__btn {
color: var(--gn);
font-weight: 700;
white-space: nowrap;
}
.export-cta-inline a:hover { text-decoration: underline; }
/* ── Mobile Bottom Navigation ── */
.bottom-nav {
@@ -1167,9 +1192,9 @@
left: 0;
right: 0;
z-index: 60;
background: var(--bg-2);
border-top: 1px solid var(--border);
padding: 4px 0;
background: #FFFFFF;
border-top: 1px solid #E2E8F0;
padding: 4px 1.5rem;
padding-bottom: max(4px, env(safe-area-inset-bottom));
}
.bottom-nav__btn {
@@ -1180,18 +1205,20 @@
gap: 2px;
padding: 6px 4px 4px;
border: none;
border-top: 2px solid transparent;
background: transparent;
color: var(--txt-3);
color: #94A3B8;
font-size: 10px;
font-weight: 600;
font-family: 'DM Sans', sans-serif;
cursor: pointer;
transition: color 0.15s;
transition: color 0.15s, border-color 0.15s;
-webkit-tap-highlight-color: transparent;
}
.bottom-nav__btn svg { transition: color 0.15s; }
.bottom-nav__btn--active {
color: var(--rd);
color: #1D4ED8;
border-top-color: #1D4ED8;
}
@media (max-width: 768px) {
.bottom-nav { display: flex; }
@@ -1206,7 +1233,6 @@
border: 1px solid var(--border);
border-radius: 14px;
background: var(--bg-2);
overflow: hidden;
}
.wizard-details__summary {
display: flex;
@@ -1252,9 +1278,8 @@
bottom: 56px; /* above bottom nav */
left: 0;
right: 0;
z-index: 55;
z-index: 65;
padding: 10px 16px;
padding-bottom: max(10px, env(safe-area-inset-bottom));
background: var(--cta-bg);
border-top: 1px solid rgba(29,78,216,0.15);
box-shadow: 0 -2px 12px rgba(29,78,216,0.08);
@@ -1336,4 +1361,12 @@
/* Add padding below content for bottom nav + CTA bar */
.planner-app main { padding-bottom: 120px; }
/* Hide feedback on planner mobile — too much competing for bottom space.
planner.css only loads on planner pages so this won't affect other pages. */
#feedback-wrap { display: none !important; }
.feedback-label { display: none; }
/* Hide live summary on mobile — wizard footer is just nav buttons */
.wizard-preview { display: none !important; }
}

View File

@@ -99,8 +99,16 @@ function showWizStep(n) {
steps.forEach(s => s.classList.toggle('active', +s.dataset.wiz === n));
document.querySelectorAll('.wiz-dot').forEach(d =>
d.classList.toggle('wiz-dot--active', +d.dataset.wiz === n));
// Fetch server-rendered nav buttons (moves i18n to templates)
htmx.ajax('GET', '/planner/wizard-nav?step=' + n, '#wizNav');
const nav = document.getElementById('wizNav');
const back = nav.dataset.back;
const next = nav.dataset.next;
const results = nav.dataset.results;
const isLast = n >= steps.length;
nav.innerHTML =
(n > 1 ? `<button type="button" class="wiz-btn--back" onclick="showWizStep(${n - 1})">${back}</button>` : '<div></div>') +
(isLast
? `<button type="button" class="wiz-btn--next" onclick="document.querySelector('.tab-btn[data-tab=\\'capex\\'],.bottom-nav__btn[data-tab=\\'capex\\']').click()">${results}</button>`
: `<button type="button" class="wiz-btn--next" onclick="showWizStep(${n + 1})">${next}</button>`);
}
// ─── Quote navigation ────────────────────────────────────────────────────────

View File

@@ -13,7 +13,7 @@
<!-- Fonts (self-hosted, @font-face in output.css) -->
<!-- Tailwind (compiled) -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}?v={{ v }}">
<!-- Umami Analytics -->
<script defer src="https://umami.padelnomics.io/Z.js" data-website-id="4474414b-58d6-4c6e-89a1-df5ea1f49d70"{% if ab_tag %} data-tag="{{ ab_tag }}"{% endif %}></script>
@@ -242,7 +242,7 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="flex-shrink:0;">
<path d="M21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17H7L3 21V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V15Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{{ t.nav_feedback }}
<span class="feedback-label">{{ t.nav_feedback }}</span>
</button>
<div id="feedback-popover" hidden
style="position:absolute;bottom:calc(100% + 0.75rem);right:0;width:300px;background:white;border:1px solid #E2E8F0;border-radius:12px;padding:1rem;box-shadow:0 8px 32px rgba(0,0,0,0.12);">