feat: results page redesign — modal form, paint-style cards, session save
The post-quiz results page is restructured around progressive disclosure
and the embossed-paint visual language used on the homepage CTAs.
Capture form
- Moved into a viewport-scaled modal with sticky header (title + close)
and sticky footer (submit). Body scrolls between them.
- Privacy note now sits inside the form, above the newsletter checkbox,
so the message is part of the consent flow rather than a footnote.
- Close button is the secondary white-with-border style; the X glyph is
drawn from two CSS strokes so it stays pixel-centred regardless of
font metrics.
- iOS-Safari-safe scroll lock: body becomes position:fixed with the
saved scrollY pinned via inline top, restored on close.
Result panels
- Recommendations / Budget / Timeline use the white-paint container
(#FAFAFA) and inner cards switch to the #F0F0F0 paint look that the
quiz q-opt items already used. Text colours flip to the dark palette
for legibility.
- Risk banner gets four paint variants — maroon (critical), amber
(high), olive (medium), green (low / "PREPARED") — each rendered via
::before with the paintGlossBtn filter. Text is warm cream #f4ecd8.
- Narrative ("Your Personal Analysis") wears the same white-paint frame
with a slightly greyer header band.
- Headings (risk title, protein offer, panel headers, narrative title)
switched from DM Serif Display to Barlow to share the button typeface.
Scenario tabs
- Container loses its border + padding; inactive tabs are quiet text
buttons. Active tab wears the green-paint CTA fill.
- Mobile lays them out as a 2×2 grid; desktop is a single row of 4.
Other tweaks
- Results section bumped 15% wider on desktop (max-width 690px).
- Site header gets a soft white fade on results-active so content
scrolling underneath doesn't clash with the logo.
- Modifier (⚙) toggle hidden via a single display:none rule, styles
preserved so we can re-enable it later.
- Protein button has a responsive label: "Secure Source Now" on mobile,
"Secure protein source now" on desktop. Arrow icons removed from
protein, retake, send-plan buttons. Unified primary-button typography
across the site (var(--font-body), 400 weight, 13px, 0.2em tracking).
Session persistence
- Quiz progress (answers, currentQ, lang, scenario, stage) is saved to
sessionStorage under "kammergut.state.v1" — purely client-side, dies
when the tab closes. Stale sessions (>4h) are discarded on load.
- Reload mid-quiz or mid-results re-enters the right view. restartQuiz
clears the saved session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
404
src/App.vue
404
src/App.vue
@@ -319,7 +319,10 @@
|
||||
<div class="protein-offer-badge">🔒 <span data-i18n="protein_offer_badge">Exclusive — DACH Region Priority Access</span></div>
|
||||
<div class="protein-offer-title" data-i18n="protein_offer_title">Secure your high-grade animal protein source</div>
|
||||
<div class="protein-offer-body" data-i18n="protein_offer_body">You indicated that your protein supply may not be secure in a crisis. We have identified an exclusive, verified source of high-grade animal protein — available with priority access for Deepstock members in Germany, Austria, and Switzerland.</div>
|
||||
<a class="protein-offer-btn" href="PROTEIN_OFFER_URL" target="_blank" rel="noopener noreferrer" data-i18n="protein_offer_btn">→ Secure My Protein Source Now</a>
|
||||
<a class="protein-offer-btn" href="PROTEIN_OFFER_URL" target="_blank" rel="noopener noreferrer">
|
||||
<span class="po-label-mobile" data-i18n="protein_offer_btn_short">Secure Source Now</span>
|
||||
<span class="po-label-desktop" data-i18n="protein_offer_btn">Secure protein source now</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- AI NARRATIVE SECTION -->
|
||||
@@ -339,123 +342,134 @@
|
||||
<div class="narrative-body hidden" id="narrative-text"></div>
|
||||
</div>
|
||||
<div class="narrative-cta hidden" id="narrative-cta">
|
||||
<button class="narrative-cta-btn" @click="scrollToForm" data-i18n="cta_scroll">
|
||||
↓ Fill in your details below to receive your personalised plan by email
|
||||
<button class="narrative-cta-btn" @click="openCaptureModal" data-i18n="cta_scroll">
|
||||
Receive Plan by email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CAPTURE FORM -->
|
||||
<div class="capture-form-wrap reveal-section" id="capture-form-wrap">
|
||||
<!-- CAPTURE FORM (modal) -->
|
||||
<div class="form-modal" id="capture-modal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="capture-modal-title" @click="onModalBackdropClick">
|
||||
<div class="form-modal-panel" @click.stop>
|
||||
<div class="form-modal-header">
|
||||
<div class="modal-title" id="capture-modal-title" data-i18n="capture_title">Personal Details</div>
|
||||
<button type="button" class="form-modal-close" @click="closeCaptureModal" aria-label="Close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="capture-title" data-i18n="capture_title">Personal Details</div>
|
||||
<div class="capture-sub" data-i18n="capture_sub">Enter your details and we'll email your complete survival plan immediately — including all recommendations below.</div>
|
||||
<div class="form-modal-body" id="form-modal-body">
|
||||
<!-- Success state replaces the form after submit -->
|
||||
<div class="capture-success hidden" id="capture-success">
|
||||
<div style="font-size:36px;margin-bottom:8px;text-align:center">✅</div>
|
||||
<div style="font-family:var(--font-display);font-weight:800;font-size:22px;color:var(--green-bright);margin-bottom:8px;text-align:center" data-i18n="success_title">Plan sent!</div>
|
||||
<div style="font-size:14px;color:var(--text-dim);line-height:1.6;text-align:center" data-i18n="success_text">Check your inbox — your personalised plan is on its way.</div>
|
||||
<div style="margin-top:14px;padding:12px 16px;background:rgba(212,168,32,0.1);border:1px solid rgba(212,168,32,0.35);border-radius:8px;text-align:center">
|
||||
<div style="font-size:13px;color:#D4A820;line-height:1.7" data-i18n="success_spam">📬 Don't see it? Check your <strong>spam or junk folder</strong>. If it's there, please mark it as <strong>"Not Spam"</strong> — this ensures future updates reach your inbox directly, and helps us improve deliverability for everyone.</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:center">
|
||||
<button class="narrative-cta-btn" @click="closeAndScrollToRecs" data-i18n="success_explore">Explore Recommendations</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="capture-form" @submit="submitCapture">
|
||||
<input type="hidden" id="h_location" name="location"/>
|
||||
<input type="hidden" id="h_household" name="household_size"/>
|
||||
<input type="hidden" id="h_water" name="water_access"/>
|
||||
<input type="hidden" id="h_food" name="food_reserves"/>
|
||||
<input type="hidden" id="h_medical" name="medical_needs"/>
|
||||
<input type="hidden" id="h_sanitation" name="sanitation"/>
|
||||
<input type="hidden" id="h_budget" name="budget_eur"/>
|
||||
<input type="hidden" id="h_priority" name="priorities"/>
|
||||
<input type="hidden" id="h_protein" name="protein_access"/>
|
||||
<input type="hidden" id="h_risk_score" name="risk_score"/>
|
||||
<input type="hidden" id="h_risk_level" name="risk_level"/>
|
||||
<input type="hidden" id="h_lang" name="language_used"/>
|
||||
<input type="hidden" id="h_timestamp" name="submitted_at"/>
|
||||
<input type="hidden" id="h_scenarios" name="scenarios"/>
|
||||
<input type="hidden" id="h_protein_pref" name="protein_preference"/>
|
||||
<input type="hidden" id="h_protein_security" name="protein_security"/>
|
||||
<div class="capture-sub" data-i18n="capture_sub">Enter your details and we'll email your complete survival plan immediately — including all recommendations below.</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_name">First Name</span> <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-input" type="text" id="f_name" name="first_name" required placeholder="First name"/>
|
||||
<form id="capture-form" @submit="submitCapture">
|
||||
<input type="hidden" id="h_location" name="location"/>
|
||||
<input type="hidden" id="h_household" name="household_size"/>
|
||||
<input type="hidden" id="h_water" name="water_access"/>
|
||||
<input type="hidden" id="h_food" name="food_reserves"/>
|
||||
<input type="hidden" id="h_medical" name="medical_needs"/>
|
||||
<input type="hidden" id="h_sanitation" name="sanitation"/>
|
||||
<input type="hidden" id="h_budget" name="budget_eur"/>
|
||||
<input type="hidden" id="h_priority" name="priorities"/>
|
||||
<input type="hidden" id="h_protein" name="protein_access"/>
|
||||
<input type="hidden" id="h_risk_score" name="risk_score"/>
|
||||
<input type="hidden" id="h_risk_level" name="risk_level"/>
|
||||
<input type="hidden" id="h_lang" name="language_used"/>
|
||||
<input type="hidden" id="h_timestamp" name="submitted_at"/>
|
||||
<input type="hidden" id="h_scenarios" name="scenarios"/>
|
||||
<input type="hidden" id="h_protein_pref" name="protein_preference"/>
|
||||
<input type="hidden" id="h_protein_security" name="protein_security"/>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_name">First Name</span> <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-input" type="text" id="f_name" name="first_name" required placeholder="First name"/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_lastname">Last Name</span> <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-input" type="text" id="f_lastname" name="last_name" required placeholder="Last name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_email">Email</span> <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-input" type="email" id="f_email" name="email" required placeholder="your@email.com"/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_city">City</span> <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-input" type="text" id="f_city" name="city" required placeholder="Your city"/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_country">Country</span> <span style="color:var(--red)">*</span></label>
|
||||
<select class="form-select" id="f_country" name="country" required @change="onCountryChange">
|
||||
<option value="">—</option>
|
||||
<option value="AT">🇦🇹 Austria</option>
|
||||
<option value="DE">🇩🇪 Germany</option>
|
||||
<option value="CH">🇨🇭 Switzerland</option>
|
||||
<option value="US">🇺🇸 United States</option>
|
||||
<option value="CA">🇨🇦 Canada</option>
|
||||
<option value="GB">🇬🇧 United Kingdom</option>
|
||||
<option value="FR">🇫🇷 France</option>
|
||||
<option value="NL">🇳🇱 Netherlands</option>
|
||||
<option value="BE">🇧🇪 Belgium</option>
|
||||
<option value="ES">🇪🇸 Spain</option>
|
||||
<option value="IT">🇮🇹 Italy</option>
|
||||
<option value="PL">🇵🇱 Poland</option>
|
||||
<option value="SE">🇸🇪 Sweden</option>
|
||||
<option value="NO">🇳🇴 Norway</option>
|
||||
<option value="DK">🇩🇰 Denmark</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_phone">Phone (optional)</span></label>
|
||||
<input class="form-input" type="tel" id="f_phone" name="phone" placeholder="+43 or +1 ..."/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_pref_lang">Preferred language</span></label>
|
||||
<select class="form-select" id="f_pref_lang" name="preferred_language">
|
||||
<option value="en">English</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="es">Español</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_protein_label">🥩 Protein access (optional)</span></label>
|
||||
<textarea class="form-input" id="f_protein_detail" name="protein_detail" rows="3"
|
||||
placeholder="e.g. free-range eggs from neighbour, fishing lake nearby, hunter in family..."></textarea>
|
||||
<div style="font-size:11px;color:var(--text-dim);margin-top:4px;font-style:italic" data-i18n="protein_note">Helps us personalise your food recommendations.</div>
|
||||
</div>
|
||||
<div class="form-privacy" data-i18n="privacy_note">🔒 Your data is never sold. Unsubscribe anytime. GDPR compliant.</div>
|
||||
<label class="newsletter-check">
|
||||
<input type="checkbox" id="f_newsletter" name="newsletter" value="yes" checked/>
|
||||
<span class="check-box"></span>
|
||||
<span class="check-label" data-i18n="newsletter_label">Yes, send me weekly preparedness updates from Deepstock</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_lastname">Last Name</span> <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-input" type="text" id="f_lastname" name="last_name" required placeholder="Last name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_email">Email</span> <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-input" type="email" id="f_email" name="email" required placeholder="your@email.com"/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_city">City</span> <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-input" type="text" id="f_city" name="city" required placeholder="Your city"/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_country">Country</span> <span style="color:var(--red)">*</span></label>
|
||||
<select class="form-select" id="f_country" name="country" required @change="onCountryChange">
|
||||
<option value="">—</option>
|
||||
<option value="AT">🇦🇹 Austria</option>
|
||||
<option value="DE">🇩🇪 Germany</option>
|
||||
<option value="CH">🇨🇭 Switzerland</option>
|
||||
<option value="US">🇺🇸 United States</option>
|
||||
<option value="CA">🇨🇦 Canada</option>
|
||||
<option value="GB">🇬🇧 United Kingdom</option>
|
||||
<option value="FR">🇫🇷 France</option>
|
||||
<option value="NL">🇳🇱 Netherlands</option>
|
||||
<option value="BE">🇧🇪 Belgium</option>
|
||||
<option value="ES">🇪🇸 Spain</option>
|
||||
<option value="IT">🇮🇹 Italy</option>
|
||||
<option value="PL">🇵🇱 Poland</option>
|
||||
<option value="SE">🇸🇪 Sweden</option>
|
||||
<option value="NO">🇳🇴 Norway</option>
|
||||
<option value="DK">🇩🇰 Denmark</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_phone">Phone (optional)</span></label>
|
||||
<input class="form-input" type="tel" id="f_phone" name="phone" placeholder="+43 or +1 ..."/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_pref_lang">Preferred language</span></label>
|
||||
<select class="form-select" id="f_pref_lang" name="preferred_language">
|
||||
<option value="en">English</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="es">Español</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="field-label"><span data-i18n="field_protein_label">🥩 Protein access (optional)</span></label>
|
||||
<textarea class="form-input" id="f_protein_detail" name="protein_detail" rows="3"
|
||||
placeholder="e.g. free-range eggs from neighbour, fishing lake nearby, hunter in family..."></textarea>
|
||||
<div style="font-size:11px;color:var(--text-dim);margin-top:4px;font-style:italic" data-i18n="protein_note">Helps us personalise your food recommendations.</div>
|
||||
</div>
|
||||
<label class="newsletter-check">
|
||||
<input type="checkbox" id="f_newsletter" name="newsletter" value="yes" checked/>
|
||||
<span class="check-box"></span>
|
||||
<span class="check-label" data-i18n="newsletter_label">Yes, send me weekly preparedness updates from Deepstock</span>
|
||||
</label>
|
||||
<button class="capture-submit" type="submit" id="capture-submit-btn">
|
||||
<span data-i18n="capture_btn">Send Me My Plan →</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="form-modal-footer" id="form-modal-footer">
|
||||
<button class="capture-submit" type="submit" form="capture-form" id="capture-submit-btn">
|
||||
<span data-i18n="capture_btn">Send Me My Plan</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="capture-success hidden" id="capture-success">
|
||||
<div style="font-size:36px;margin-bottom:8px;text-align:center">✅</div>
|
||||
<div style="font-family:var(--font-display);font-weight:800;font-size:22px;color:var(--green-bright);margin-bottom:8px;text-align:center" data-i18n="success_title">Plan sent!</div>
|
||||
<div style="font-size:14px;color:var(--text-dim);line-height:1.6;text-align:center" data-i18n="success_text">Check your inbox — your personalised plan is on its way.</div>
|
||||
<div style="margin-top:14px;padding:12px 16px;background:rgba(212,168,32,0.1);border:1px solid rgba(212,168,32,0.35);border-radius:8px;text-align:center">
|
||||
<div style="font-size:13px;color:#D4A820;line-height:1.7" data-i18n="success_spam">📬 Don't see it? Check your <strong>spam or junk folder</strong>. If it's there, please mark it as <strong>"Not Spam"</strong> — this ensures future updates reach your inbox directly, and helps us improve deliverability for everyone.</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:center">
|
||||
<button class="narrative-cta-btn" @click="scrollToRecs" data-i18n="success_explore">↓ Now explore your full recommendations below</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:12px;text-align:center;line-height:1.7" data-i18n="privacy_note">🔒 Your data is never sold. Unsubscribe anytime. GDPR compliant.</div>
|
||||
</div>
|
||||
|
||||
<!-- RECOMMENDATIONS + SCENARIO TABS — collapsible panels -->
|
||||
@@ -500,7 +514,7 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<button class="restart-btn" @click="restartQuiz" data-i18n="restart_btn">↩ Retake Assessment</button>
|
||||
<button class="restart-btn" @click="restartQuiz" data-i18n="restart_btn">Retake Assessment</button>
|
||||
</section>
|
||||
|
||||
<!-- ABOUT -->
|
||||
@@ -556,7 +570,7 @@ const T = {
|
||||
panel_recs_hint: "Tap a scenario to compare",
|
||||
panel_budget: "💰 Budget Plan",
|
||||
panel_timeline: "⏱ Action Timeline",
|
||||
restart_btn: "↩ Retake Assessment",
|
||||
restart_btn: "Retake Assessment",
|
||||
about_title: "Why Deepstock?",
|
||||
about_text: "Built by preparedness researchers and city-dwelling practitioners. Every recommendation is sourced, tested, and city-apartment-compatible.",
|
||||
affiliate_note: "* This site uses affiliate links. When you purchase through our links, we may earn a commission at no extra cost to you. This helps keep the platform free.",
|
||||
@@ -596,10 +610,11 @@ const T = {
|
||||
protein_offer_badge:"Exclusive — DACH Region Priority Access",
|
||||
protein_offer_title:"Secure your high-grade animal protein source",
|
||||
protein_offer_body:"You indicated your protein supply may not be secure in a crisis. We have identified an exclusive, verified source of high-grade animal protein — available with priority access for Deepstock members in Germany, Austria, and Switzerland.",
|
||||
protein_offer_btn:"→ Secure My Protein Source Now",
|
||||
protein_offer_btn:"Secure protein source now",
|
||||
protein_offer_btn_short:"Secure Source Now",
|
||||
capture_header:"Personal Details",
|
||||
cta_scroll:"↓ Fill in your details below to receive your personalised plan by email",
|
||||
success_explore:"↓ Now explore your full recommendations below",
|
||||
cta_scroll:"Receive Plan by email",
|
||||
success_explore:"Explore Recommendations",
|
||||
success_spam:"📬 Don't see it? Check your spam or junk folder. If it's there, please mark it as Not Spam — this ensures future updates reach your inbox directly, and helps us improve deliverability for everyone.",
|
||||
view_amazon: "View on Amazon",
|
||||
bcat_water: "Water",
|
||||
@@ -655,7 +670,7 @@ const T = {
|
||||
panel_recs_hint: "Szenario antippen zum Vergleichen",
|
||||
panel_budget: "💰 Budgetplan",
|
||||
panel_timeline: "⏱ Aktionsplan",
|
||||
restart_btn: "↩ Neu starten",
|
||||
restart_btn: "Neu starten",
|
||||
about_title: "Warum Kammergut?",
|
||||
about_text: "Entwickelt von Vorsorge-Forschern und Stadtbewohnern. Jede Empfehlung ist recherchiert, getestet und für Stadtwohnungen geeignet.",
|
||||
affiliate_note: "* Diese Website nutzt Affiliate-Links. Beim Kauf über unsere Links erhalten wir eine Provision ohne Mehrkosten für dich. So bleibt die Plattform kostenlos.",
|
||||
@@ -695,10 +710,11 @@ const T = {
|
||||
protein_offer_badge:"Exklusiv — DACH Region Prioritätszugang",
|
||||
protein_offer_title:"Sichere deine hochwertige tierische Proteinquelle",
|
||||
protein_offer_body:"Du hast angegeben, dass deine Proteinversorgung in einer Krise möglicherweise nicht gesichert ist. Wir haben eine exklusive, geprüfte Quelle für hochwertiges tierisches Protein identifiziert — mit Prioritätszugang für Kammergut Mitglieder in Deutschland, Österreich und der Schweiz.",
|
||||
protein_offer_btn:"→ Jetzt Proteinquelle sichern",
|
||||
protein_offer_btn:"Proteinquelle sichern",
|
||||
protein_offer_btn_short:"Quelle sichern",
|
||||
capture_header:"Persönliche Daten",
|
||||
cta_scroll:"↓ Fülle deine Daten aus um deinen personalisierten Plan per E-Mail zu erhalten",
|
||||
success_explore:"↓ Entdecke jetzt deine vollständigen Empfehlungen unten",
|
||||
cta_scroll:"Plan per E-Mail erhalten",
|
||||
success_explore:"Empfehlungen ansehen",
|
||||
success_spam:"📬 Nicht erhalten? Schau in deinen Spam-Ordner. Falls du es dort findest, markiere es bitte als Kein Spam — so landen zukünftige Updates direkt in deinem Posteingang und du hilfst uns die Zustellbarkeit für alle zu verbessern.",
|
||||
view_amazon: "Bei Amazon ansehen",
|
||||
bcat_water: "Wasser",
|
||||
@@ -739,6 +755,7 @@ function setLang(lang) {
|
||||
el.placeholder = el.getAttribute('data-ph-' + lang)
|
||||
})
|
||||
if (currentQ !== null && currentQ < QUESTIONS.length) renderQuestion()
|
||||
saveState()
|
||||
}
|
||||
|
||||
function t(key) { return T[currentLang][key] || T['en'][key] || key }
|
||||
@@ -880,6 +897,44 @@ let currentScenario = 1
|
||||
let riskScore = 0
|
||||
let riskLevelStr = ''
|
||||
|
||||
// Session persistence — keep the user's progress across reloads inside
|
||||
// the same browser tab. `sessionStorage` is the right scope: progress
|
||||
// dies when the tab closes (privacy-friendly default for a lead form),
|
||||
// but reload / nav-away-and-back restores. Bump the key suffix when the
|
||||
// QUESTIONS shape changes incompatibly.
|
||||
const STATE_KEY = 'kammergut.state.v1'
|
||||
function getStage() {
|
||||
const results = document.getElementById('results-section')
|
||||
const quiz = document.getElementById('quiz-section')
|
||||
if (results && results.classList.contains('active')) return 'results'
|
||||
if (quiz && !quiz.classList.contains('hidden')) return 'quiz'
|
||||
return 'home'
|
||||
}
|
||||
function saveState() {
|
||||
const stage = getStage()
|
||||
if (stage === 'home') { clearState(); return }
|
||||
try {
|
||||
sessionStorage.setItem(STATE_KEY, JSON.stringify({
|
||||
stage, currentQ, answers, currentScenario,
|
||||
currentLang, ts: Date.now(),
|
||||
}))
|
||||
} catch {}
|
||||
}
|
||||
function clearState() {
|
||||
try { sessionStorage.removeItem(STATE_KEY) } catch {}
|
||||
}
|
||||
function loadState() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STATE_KEY)
|
||||
if (!raw) return null
|
||||
const s = JSON.parse(raw)
|
||||
// Discard sessions older than 4 hours so a stale tab doesn't drop
|
||||
// the user mid-flow on a different day.
|
||||
if (s.ts && Date.now() - s.ts > 4 * 60 * 60 * 1000) return null
|
||||
return s
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════
|
||||
// QUIZ FLOW
|
||||
// ══════════════════════════════════════
|
||||
@@ -979,6 +1034,7 @@ function selectOption(qid, val, el) {
|
||||
document.querySelectorAll('.q-opt').forEach(o => o.classList.remove('selected'))
|
||||
el.classList.add('selected')
|
||||
document.getElementById('btn-next').disabled = false
|
||||
saveState()
|
||||
}
|
||||
|
||||
function toggleMulti(qid, val, el) {
|
||||
@@ -986,19 +1042,21 @@ function toggleMulti(qid, val, el) {
|
||||
const idx = answers[qid].indexOf(val)
|
||||
if (idx >= 0) { answers[qid].splice(idx, 1); el.classList.remove('selected') }
|
||||
else { answers[qid].push(val); el.classList.add('selected') }
|
||||
saveState()
|
||||
}
|
||||
|
||||
function updateSlider(val, qid) {
|
||||
answers[qid] = parseInt(val)
|
||||
document.getElementById('slider-display').innerHTML = `€${parseInt(val).toLocaleString()}`
|
||||
saveState()
|
||||
}
|
||||
|
||||
function nextQ() {
|
||||
if (currentQ < QUESTIONS.length - 1) { currentQ++; renderQuestion() }
|
||||
if (currentQ < QUESTIONS.length - 1) { currentQ++; renderQuestion(); saveState() }
|
||||
else showResults()
|
||||
}
|
||||
|
||||
function prevQ() { if (currentQ > 0) { currentQ--; renderQuestion() } }
|
||||
function prevQ() { if (currentQ > 0) { currentQ--; renderQuestion(); saveState() } }
|
||||
|
||||
function restartQuiz() {
|
||||
const rs = document.getElementById('results-section')
|
||||
@@ -1012,6 +1070,11 @@ function restartQuiz() {
|
||||
const quiz = document.getElementById('quiz-section')
|
||||
if(quiz) quiz.classList.add('hidden')
|
||||
document.body.classList.remove('quiz-active')
|
||||
document.body.classList.remove('results-active')
|
||||
|
||||
// Wipe any in-progress session state so the home view is fresh.
|
||||
currentQ = 0; answers = {}; currentScenario = 1; riskScore = 0; riskLevelStr = ''
|
||||
clearState()
|
||||
|
||||
// Restore the locked-viewport layout that startQuiz had to undo for
|
||||
// the scrollable quiz/results pages — without this the hero on the
|
||||
@@ -1181,10 +1244,12 @@ function getScenarioRecs(scenario) {
|
||||
function showResults() {
|
||||
document.getElementById('quiz-section').classList.add('hidden')
|
||||
document.body.classList.remove('quiz-active')
|
||||
document.body.classList.add('results-active')
|
||||
const results = document.getElementById('results-section')
|
||||
results.style.display = 'block'
|
||||
results.classList.add('active')
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
saveState()
|
||||
|
||||
riskScore = calcRisk()
|
||||
checkProteinOffer()
|
||||
@@ -1220,6 +1285,7 @@ function showResults() {
|
||||
|
||||
function showScenario(n) {
|
||||
currentScenario = n
|
||||
saveState()
|
||||
document.querySelectorAll('.s-tab').forEach(tab => {
|
||||
tab.classList.remove('active-s1','active-s2','active-s3','active-s4')
|
||||
if (parseInt(tab.dataset.s) === n) tab.classList.add(`active-s${n}`)
|
||||
@@ -1359,8 +1425,12 @@ function submitCapture(e) {
|
||||
|
||||
const showSuccess = () => {
|
||||
const form = document.getElementById('capture-form')
|
||||
const sub = document.querySelector('#capture-modal .capture-sub')
|
||||
const success = document.getElementById('capture-success')
|
||||
const footer = document.getElementById('form-modal-footer')
|
||||
if(form) form.style.display = 'none'
|
||||
if(sub) sub.style.display = 'none'
|
||||
if(footer) footer.style.display = 'none'
|
||||
if(success) { success.classList.remove('hidden'); success.style.display = 'block' }
|
||||
}
|
||||
|
||||
@@ -1545,14 +1615,10 @@ function getFallbackNarrativeText() {
|
||||
}
|
||||
|
||||
function revealSections() {
|
||||
// Capture form first, then the three collapsible result panels (recs is open
|
||||
// by default, budget + timeline collapsed for progressive disclosure).
|
||||
const ids = [
|
||||
'capture-form-wrap',
|
||||
'panel-recs',
|
||||
'panel-budget',
|
||||
'panel-timeline',
|
||||
]
|
||||
// The three collapsible result panels (recs is open by default,
|
||||
// budget + timeline collapsed for progressive disclosure). The capture
|
||||
// form lives in a modal now and reveals on click instead of scroll.
|
||||
const ids = ['panel-recs', 'panel-budget', 'panel-timeline']
|
||||
ids.forEach((id, i) => {
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(id)
|
||||
@@ -1561,14 +1627,41 @@ function revealSections() {
|
||||
})
|
||||
}
|
||||
|
||||
function scrollToForm() {
|
||||
const el = document.getElementById('capture-form-wrap')
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
// Scroll-lock the page behind the modal. Saving and restoring scrollY +
|
||||
// position:fixed on body is the iOS-Safari-safe pattern; plain
|
||||
// overflow:hidden alone doesn't stop touch-driven body scroll there.
|
||||
let savedScrollY = 0
|
||||
function openCaptureModal() {
|
||||
const modal = document.getElementById('capture-modal')
|
||||
if (!modal) return
|
||||
savedScrollY = window.scrollY || window.pageYOffset || 0
|
||||
modal.classList.add('open')
|
||||
modal.setAttribute('aria-hidden', 'false')
|
||||
document.body.style.top = `-${savedScrollY}px`
|
||||
document.body.classList.add('modal-open')
|
||||
}
|
||||
function closeCaptureModal() {
|
||||
const modal = document.getElementById('capture-modal')
|
||||
if (!modal) return
|
||||
modal.classList.remove('open')
|
||||
modal.setAttribute('aria-hidden', 'true')
|
||||
document.body.classList.remove('modal-open')
|
||||
document.body.style.top = ''
|
||||
window.scrollTo(0, savedScrollY)
|
||||
}
|
||||
function onModalBackdropClick(e) {
|
||||
// Only close when the click is on the overlay itself (the panel stops
|
||||
// propagation so clicks inside the form don't bubble up).
|
||||
if (e.target === e.currentTarget) closeCaptureModal()
|
||||
}
|
||||
function scrollToRecs() {
|
||||
const el = document.getElementById('recs-anchor')
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
function closeAndScrollToRecs() {
|
||||
closeCaptureModal()
|
||||
setTimeout(() => scrollToRecs(), 250)
|
||||
}
|
||||
|
||||
function checkProteinOffer() {
|
||||
const offerEl = document.getElementById('protein-offer-section')
|
||||
@@ -1778,16 +1871,63 @@ onMounted(() => {
|
||||
window.nextQ = nextQ
|
||||
window.prevQ = prevQ
|
||||
window.showScenario = showScenario
|
||||
window.scrollToForm = scrollToForm
|
||||
window.openCaptureModal = openCaptureModal
|
||||
window.closeCaptureModal = closeCaptureModal
|
||||
window.scrollToRecs = scrollToRecs
|
||||
window.closeAndScrollToRecs = closeAndScrollToRecs
|
||||
window.updateRegionFromCountry = updateRegionFromCountry
|
||||
window.updateRegionIndicator = updateRegionIndicator
|
||||
window.showRegionPicker = showRegionPicker
|
||||
|
||||
// Initial render
|
||||
setLang('en')
|
||||
// ESC closes the capture modal
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.getElementById('capture-modal')
|
||||
if (modal && modal.classList.contains('open')) closeCaptureModal()
|
||||
}
|
||||
})
|
||||
|
||||
// Initial render — pull saved session state (if any) so reload mid-quiz
|
||||
// restores the user where they left off. Lang sets first so renderQuestion
|
||||
// / renderResults pick up the right strings.
|
||||
const saved = loadState()
|
||||
const lang = (saved && saved.currentLang) || 'en'
|
||||
setLang(lang)
|
||||
updateRegionIndicator()
|
||||
|
||||
if (saved && saved.stage && saved.stage !== 'home') {
|
||||
answers = saved.answers || {}
|
||||
currentQ = typeof saved.currentQ === 'number' ? saved.currentQ : 0
|
||||
currentScenario = saved.currentScenario || 1
|
||||
if (saved.stage === 'quiz') {
|
||||
// Re-enter quiz mode without resetting state — startQuiz wipes
|
||||
// answers, so call its DOM-only side effects then renderQuestion.
|
||||
const hide = id => { const el = document.getElementById(id) || document.querySelector('.' + id); if(el) el.classList.add('hidden') }
|
||||
hide('hero-section'); hide('about-section')
|
||||
const strip = document.querySelector('.scenario-strip')
|
||||
if(strip) strip.classList.add('hidden')
|
||||
document.body.style.overflow = 'auto'
|
||||
document.body.style.height = 'auto'
|
||||
const app = document.querySelector('.app')
|
||||
if (app) { app.style.overflow = 'visible'; app.style.height = 'auto' }
|
||||
const quizEl = document.getElementById('quiz-section')
|
||||
if(quizEl) quizEl.classList.remove('hidden')
|
||||
document.body.classList.add('quiz-active')
|
||||
renderQuestion()
|
||||
} else if (saved.stage === 'results') {
|
||||
// Same DOM unwind as quiz, then jump straight to results.
|
||||
document.body.style.overflow = 'auto'
|
||||
document.body.style.height = 'auto'
|
||||
const app = document.querySelector('.app')
|
||||
if (app) { app.style.overflow = 'visible'; app.style.height = 'auto' }
|
||||
const hero = document.getElementById('hero-section')
|
||||
if(hero) hero.classList.add('hidden')
|
||||
const strip = document.querySelector('.scenario-strip')
|
||||
if(strip) strip.classList.add('hidden')
|
||||
showResults()
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up scenario strip + paint picker after DOM is mounted
|
||||
initScenarioStrip()
|
||||
initPaintPicker()
|
||||
|
||||
472
src/styles.css
472
src/styles.css
@@ -494,63 +494,213 @@ input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 22px;
|
||||
.btn-next:disabled { background: var(--muted); box-shadow: none; cursor: not-allowed; color: var(--deep); }
|
||||
|
||||
/* ── RESULTS ── */
|
||||
.results-section { padding: 24px 20px 80px; max-width: 600px; margin: 0 auto; width: 100%; display: none; }
|
||||
.results-section { padding: 84px 20px 80px; max-width: 600px; margin: 0 auto; width: 100%; display: none; }
|
||||
@media (min-width: 768px) {
|
||||
/* Desktop — bump the content column ~15% so the result panels and
|
||||
recommendation cards have more breathing room on a wide viewport. */
|
||||
.results-section { max-width: 690px; }
|
||||
}
|
||||
.results-section.active { display: block; }
|
||||
.risk-banner { padding: 24px; border-radius: var(--radius-lg); margin-bottom: 24px; text-align: center; animation: fadeUp 0.4s ease both; }
|
||||
.risk-level { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; margin-bottom: 8px; opacity: 0.8; }
|
||||
.risk-title { font-family: var(--font-display); font-weight: 900; font-size: 36px; letter-spacing: -0.01em; line-height: 1; margin-bottom: 8px; }
|
||||
.risk-desc { font-size: 14px; opacity: 0.85; line-height: 1.6; }
|
||||
.risk-critical { background: var(--red-dim); border: 1px solid rgba(90,154,120,0.35); color: var(--white); }
|
||||
.risk-high { background: var(--orange-dim); border: 1px solid rgba(74,138,104,0.35); color: var(--white); }
|
||||
.risk-medium { background: var(--accent-dim); border: 1px solid rgba(90,154,120,0.25); color: var(--white); }
|
||||
.risk-low { background: var(--green-dim); border: 1px solid rgba(90,154,120,0.35); color: var(--white); }
|
||||
/* Risk banner — wrapper provides positioning + padding so risk-* variants
|
||||
can paint their fill via ::before (the dark-green paint variant uses an
|
||||
SVG gloss filter that needs an isolated stacking context). */
|
||||
.risk-banner {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
padding: 28px 24px;
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
animation: fadeUp 0.4s ease both;
|
||||
}
|
||||
.risk-level { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; margin-bottom: 10px; opacity: 0.85; }
|
||||
/* Heading typeface unified with the button font (Barlow / var(--font-body))
|
||||
instead of the DM Serif display face — keeps the assessment headings in
|
||||
the same family as the CTA buttons. */
|
||||
.risk-title {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 800;
|
||||
font-size: 36px;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.risk-desc { font-size: 14px; opacity: 0.9; line-height: 1.6; }
|
||||
/* Risk variants — each state wears a paint card in its own deep tint.
|
||||
::before carries the painted fill (paintGlossBtn filter for the glossy
|
||||
embossed look that the CTA buttons use); the wrapper itself is just
|
||||
the drop shadow + positioning context. Text colours flip to warm cream
|
||||
for legibility on the dark backgrounds. */
|
||||
.risk-critical,
|
||||
.risk-high,
|
||||
.risk-medium,
|
||||
.risk-low {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: 0 8px 18px rgba(0,0,0,0.18);
|
||||
}
|
||||
.risk-critical::before,
|
||||
.risk-high::before,
|
||||
.risk-medium::before,
|
||||
.risk-low::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
z-index: -1;
|
||||
filter: url(#paintGlossBtn);
|
||||
-webkit-filter: url(#paintGlossBtn);
|
||||
}
|
||||
.risk-critical::before { background: #5A1818; } /* deep maroon — critical */
|
||||
.risk-high::before { background: #5A3818; } /* deep amber — high */
|
||||
.risk-medium::before { background: #4A3A10; } /* dark olive — moderate */
|
||||
.risk-low::before { background: #2A3010; } /* dark green — prepared */
|
||||
|
||||
.scenario-tabs { display: flex; gap: 0; background: var(--panel); border: 1px solid var(--border); border-radius: 0; padding: 4px; margin-bottom: 24px; overflow: hidden; animation: fadeUp 0.4s 0.1s ease both; }
|
||||
.s-tab { flex: 1; padding: 10px 6px; border: none; background: none; border-radius: 0; font-family: var(--font-display); font-weight: 700; font-size: 12px; letter-spacing: 0.04em; color: var(--text-dim); cursor: pointer; transition: var(--trans); text-align: center; line-height: 1.3; }
|
||||
.s-tab.active-s1 { background: rgba(90,154,120,0.1); color: var(--red); }
|
||||
.s-tab.active-s2 { background: rgba(184,152,106,0.1); color: var(--orange); }
|
||||
.s-tab.active-s3 { background: rgba(90,154,120,0.08); color: var(--yellow); }
|
||||
.s-tab.active-s4 { background: rgba(106,170,138,0.1); color: var(--green-bright); }
|
||||
.risk-critical .risk-level,
|
||||
.risk-critical .risk-title,
|
||||
.risk-critical .risk-desc,
|
||||
.risk-high .risk-level,
|
||||
.risk-high .risk-title,
|
||||
.risk-high .risk-desc,
|
||||
.risk-medium .risk-level,
|
||||
.risk-medium .risk-title,
|
||||
.risk-medium .risk-desc,
|
||||
.risk-low .risk-level,
|
||||
.risk-low .risk-title,
|
||||
.risk-low .risk-desc { color: #f4ecd8; }
|
||||
|
||||
/* Scenario tabs — flat row inside the white-paint Recommendations panel.
|
||||
No outer container border/padding (the panel frame already provides
|
||||
that); inactive tabs are quiet text buttons. The active tab wears the
|
||||
green-paint CTA look (same paint gloss + #2a3010 fill as the protein
|
||||
button) so the current scenario reads at a glance. */
|
||||
.scenario-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 16px;
|
||||
animation: fadeUp 0.4s 0.1s ease both;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
/* Desktop has room for one row of 4. */
|
||||
.scenario-tabs { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
.s-tab {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
padding: 10px 6px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.s-tab:hover { color: var(--text); }
|
||||
/* Active state — green-paint button. ::before holds the glossy fill so
|
||||
the text colour stays solid against the dark green. */
|
||||
.s-tab.active-s1,
|
||||
.s-tab.active-s2,
|
||||
.s-tab.active-s3,
|
||||
.s-tab.active-s4 {
|
||||
color: #f4ecd8;
|
||||
}
|
||||
.s-tab.active-s1::before,
|
||||
.s-tab.active-s2::before,
|
||||
.s-tab.active-s3::before,
|
||||
.s-tab.active-s4::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
z-index: -1;
|
||||
background: #2a3010;
|
||||
filter: url(#paintGlossBtn);
|
||||
-webkit-filter: url(#paintGlossBtn);
|
||||
transition: filter 0.6s ease;
|
||||
}
|
||||
|
||||
/* Recommendation cards — wear the white-paint inner-item look (#F0F0F0 +
|
||||
embossed shadow + inset highlight), parallel to the .q-opt items inside
|
||||
the question card. The rec-cards sit inside the .result-panel which is
|
||||
#FAFAFA, mirroring the question-card / q-opt nesting. Text colours flip
|
||||
to dark for legibility on the light background. */
|
||||
.rec-cards { animation: fadeUp 0.4s 0.15s ease both; }
|
||||
.rec-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg); margin-bottom: 16px; overflow: hidden; }
|
||||
.rec-header { padding: 16px 20px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--border); }
|
||||
.rec-card {
|
||||
background: #F0F0F0;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.7);
|
||||
}
|
||||
.rec-header { padding: 14px 16px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid rgba(0,0,0,0.06); }
|
||||
.rec-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
|
||||
.rec-cat { font-family: var(--font-mono); font-size: 10px; color: var(--text-dim); letter-spacing: 0.12em; text-transform: uppercase; }
|
||||
.rec-title { font-family: var(--font-display); font-weight: 400; font-size: 18px; color: var(--white); line-height: 1.2; }
|
||||
.rec-title { font-family: var(--font-display); font-weight: 400; font-size: 18px; color: var(--text); line-height: 1.2; }
|
||||
.priority-badge { margin-left: auto; padding: 4px 10px; border-radius: 99px; font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; font-weight: 700; flex-shrink: 0; }
|
||||
.p-critical { background: rgba(90,154,120,0.08); color: var(--red); border: 1px solid rgba(90,154,120,0.2); }
|
||||
.p-high { background: rgba(184,152,106,0.08); color: var(--orange); border: 1px solid rgba(184,152,106,0.2); }
|
||||
.p-medium { background: rgba(90,154,120,0.06); color: var(--yellow); border: 1px solid rgba(90,154,120,0.15); }
|
||||
.p-critical { background: rgba(90,154,120,0.10); color: var(--red); border: 1px solid rgba(90,154,120,0.30); }
|
||||
.p-high { background: rgba(184,152,106,0.12); color: var(--orange); border: 1px solid rgba(184,152,106,0.30); }
|
||||
.p-medium { background: rgba(90,154,120,0.08); color: var(--green-bright); border: 1px solid rgba(90,154,120,0.20); }
|
||||
|
||||
.rec-body { padding: 16px 20px; }
|
||||
.rec-body { padding: 14px 16px; }
|
||||
.rec-items { display: flex; flex-direction: column; gap: 12px; }
|
||||
.rec-item { display: flex; flex-direction: column; gap: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
|
||||
.rec-item { display: flex; flex-direction: column; gap: 8px; padding-bottom: 12px; border-bottom: 1px solid rgba(0,0,0,0.06); }
|
||||
.rec-item:last-child { border-bottom: none; padding-bottom: 0; }
|
||||
.item-name { font-weight: 600; font-size: 15px; color: var(--bright); }
|
||||
.item-name { font-weight: 600; font-size: 15px; color: var(--text); }
|
||||
.item-why { font-size: 13px; color: var(--text-dim); line-height: 1.55; }
|
||||
.item-cost { font-family: var(--font-mono); font-size: 13px; color: var(--green-bright); }
|
||||
.affiliate-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; background: var(--accent-dim); border: 1px solid var(--accent); border-radius: 0; color: #88BBFF; font-size: 12px; font-weight: 600; text-decoration: none; cursor: pointer; transition: var(--trans); width: fit-content; }
|
||||
.affiliate-btn:hover { background: rgba(74,143,224,0.2); }
|
||||
.affiliate-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: rgba(0,0,0,0.04);
|
||||
border: 1px solid rgba(0,0,0,0.10);
|
||||
border-radius: 0;
|
||||
color: var(--text);
|
||||
font-size: 12px; font-weight: 600;
|
||||
text-decoration: none; cursor: pointer;
|
||||
transition: var(--trans); width: fit-content;
|
||||
}
|
||||
.affiliate-btn:hover { background: rgba(0,0,0,0.08); }
|
||||
|
||||
/* Budget meter — same logic as the rec-cards: text flips to dark since
|
||||
the .result-panel underneath is white-paint. The .budget-meter wrapper
|
||||
itself is stripped to transparent in the panel-body override (line ~615),
|
||||
so only the inner element colours need adjusting here. */
|
||||
.budget-meter { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 20px; animation: fadeUp 0.4s 0.2s ease both; }
|
||||
.bm-title { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--white); margin-bottom: 16px; }
|
||||
.bm-title { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--text); margin-bottom: 16px; }
|
||||
.budget-cats { display: flex; flex-direction: column; gap: 10px; }
|
||||
.bcat { display: flex; align-items: center; gap: 10px; }
|
||||
.bcat-label { font-size: 13px; color: var(--text-dim); width: 100px; flex-shrink: 0; }
|
||||
.bcat-bar-wrap { flex: 1; height: 6px; background: var(--muted); border-radius: 99px; overflow: hidden; }
|
||||
.bcat-bar-wrap { flex: 1; height: 6px; background: rgba(0,0,0,0.08); border-radius: 99px; overflow: hidden; }
|
||||
.bcat-bar { height: 100%; border-radius: 99px; transition: width 0.8s cubic-bezier(0.4,0,0.2,1); }
|
||||
.bcat-cost { font-family: var(--font-mono); font-size: 12px; color: var(--bright); width: 60px; text-align: right; flex-shrink: 0; }
|
||||
.bcat-cost { font-family: var(--font-mono); font-size: 12px; color: var(--text); width: 60px; text-align: right; flex-shrink: 0; }
|
||||
|
||||
/* Timeline — same dark-on-light flip. Connector line uses a subtle dark
|
||||
border colour so it reads on the white-paint panel. */
|
||||
.timeline { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 20px; animation: fadeUp 0.4s 0.25s ease both; }
|
||||
.tl-title { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--white); margin-bottom: 16px; }
|
||||
.tl-title { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--text); margin-bottom: 16px; }
|
||||
.tl-items { display: flex; flex-direction: column; gap: 0; }
|
||||
.tl-item { display: flex; gap: 16px; padding-bottom: 16px; position: relative; }
|
||||
.tl-item:last-child { padding-bottom: 0; }
|
||||
.tl-item:not(:last-child)::before { content: ''; position: absolute; left: 15px; top: 32px; bottom: 0; width: 2px; background: var(--border); }
|
||||
.tl-dot { width: 32px; height: 32px; border-radius: 50%; background: var(--panel); border: 2px solid; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; position: relative; z-index: 1; }
|
||||
.tl-item:not(:last-child)::before { content: ''; position: absolute; left: 15px; top: 32px; bottom: 0; width: 2px; background: rgba(0,0,0,0.10); }
|
||||
.tl-dot { width: 32px; height: 32px; border-radius: 50%; background: #F0F0F0; border: 2px solid; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; position: relative; z-index: 1; }
|
||||
.tl-when { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 4px; }
|
||||
.tl-action { font-size: 14px; color: var(--bright); line-height: 1.5; }
|
||||
.tl-action { font-size: 14px; color: var(--text); line-height: 1.5; }
|
||||
.tl-cost { font-family: var(--font-mono); font-size: 12px; color: var(--green-bright); margin-top: 4px; }
|
||||
|
||||
/* ── RESULT PANELS — collapsible accordions for progressive disclosure.
|
||||
@@ -580,12 +730,12 @@ input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 22px;
|
||||
.result-panel-header::-webkit-details-marker { display: none; }
|
||||
.result-panel-header:hover { background: rgba(0,0,0,0.02); }
|
||||
.rp-title {
|
||||
font-family: var(--font-display);
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
font-size: 16px;
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
letter-spacing: 0.01em;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.rp-hint {
|
||||
@@ -656,6 +806,29 @@ input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 22px;
|
||||
/* ── SINGLE FOLD: hide non-essential sections on load ──
|
||||
(header stays visible with a transparent background on the homepage) */
|
||||
.site-header { background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; border-bottom: none; }
|
||||
|
||||
/* Results-page header — soft white fade so content doesn't visually
|
||||
clash with the logo / language toggle when scrolling underneath
|
||||
(especially noticeable on mobile). The header itself becomes a
|
||||
translucent white veil; a ::after pseudo extends ~28px below it
|
||||
with a fade-to-transparent so the boundary feels organic. */
|
||||
body.results-active .site-header {
|
||||
background: rgba(250,250,250,0.86);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
body.results-active .site-header::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 28px;
|
||||
background: linear-gradient(180deg, rgba(250,250,250,0.86) 0%, rgba(250,250,250,0) 100%);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.scenario-strip,
|
||||
.email-section,
|
||||
.about-section { display: none !important; }
|
||||
@@ -738,6 +911,9 @@ body.paint-green .qpb-logo {
|
||||
}
|
||||
|
||||
/* Modifier toggle ⚙ — wears the same white-paint look as the lang-toggle */
|
||||
/* Hidden for now — styles preserved so we can re-enable the modifier
|
||||
panel later by removing this single rule. */
|
||||
.mod-open-btn { display: none !important; }
|
||||
.mod-open-btn {
|
||||
background: #F0F0F0;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
@@ -907,27 +1083,33 @@ body.mod-open .mod-panel { display: block; }
|
||||
|
||||
|
||||
/* ── NARRATIVE SECTION ── */
|
||||
/* Narrative card — white-paint container that matches the form / result
|
||||
panels (#FAFAFA, embossed shadow, inset highlight). The header takes a
|
||||
slightly greyer tint (#F0F0F0) so it reads as a subtle band above the
|
||||
body — same nesting as the result-panel header. */
|
||||
.narrative-section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
background: #FAFAFA;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 24px;
|
||||
overflow: hidden;
|
||||
animation: fadeUp 0.4s ease both;
|
||||
box-shadow: 0 7px 10.6px rgba(0,0,0,0.12), inset 0 1px 0 rgba(255,255,255,0.7);
|
||||
}
|
||||
.narrative-header {
|
||||
padding: 16px 20px;
|
||||
background: rgba(90,154,120,0.03);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 14px 18px;
|
||||
background: #F0F0F0;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.narrative-title {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 800;
|
||||
font-size: 17px;
|
||||
color: var(--white);
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.narrative-tag {
|
||||
margin-left: auto;
|
||||
@@ -950,7 +1132,7 @@ body.mod-open .mod-panel { display: block; }
|
||||
font-family: var(--font-display);
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
color: var(--white);
|
||||
color: var(--text);
|
||||
margin: 20px 0 8px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
@@ -958,7 +1140,7 @@ body.mod-open .mod-panel { display: block; }
|
||||
.narrative-body p { margin: 0 0 12px; }
|
||||
.narrative-body ul { margin: 8px 0 12px; padding-left: 20px; }
|
||||
.narrative-body li { margin-bottom: 6px; }
|
||||
.narrative-body strong { color: var(--bright); }
|
||||
.narrative-body strong { color: var(--text); font-weight: 700; }
|
||||
.narrative-loading {
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
@@ -1136,13 +1318,14 @@ body.mod-open .mod-panel { display: block; }
|
||||
|
||||
/* ── PROPRIETARY PROTEIN OFFER ── */
|
||||
.protein-offer {
|
||||
background: var(--green-dim);
|
||||
border: 1px solid rgba(90,154,120,0.45);
|
||||
background: #E5F0E0;
|
||||
border: 1px solid rgba(90,154,120,0.30);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
animation: fadeUp 0.5s ease both;
|
||||
display: none;
|
||||
box-shadow: 0 7px 10.6px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.7);
|
||||
}
|
||||
.protein-offer.show { display: block; }
|
||||
.protein-offer-badge {
|
||||
@@ -1161,12 +1344,13 @@ body.mod-open .mod-panel { display: block; }
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.protein-offer-title {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 900;
|
||||
font-size: 22px;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
color: var(--white);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.protein-offer-body {
|
||||
font-size: 14px;
|
||||
@@ -1195,6 +1379,12 @@ body.mod-open .mod-panel { display: block; }
|
||||
justify-content: center;
|
||||
}
|
||||
.protein-offer-btn:hover { background: #32C070; transform: translateY(-1px); }
|
||||
/* Responsive label — short on mobile (narrow card), long on desktop. */
|
||||
.protein-offer-btn .po-label-desktop { display: none; }
|
||||
@media (min-width: 768px) {
|
||||
.protein-offer-btn .po-label-mobile { display: none; }
|
||||
.protein-offer-btn .po-label-desktop { display: inline; }
|
||||
}
|
||||
|
||||
.scenario-strip{
|
||||
overflow:hidden;
|
||||
@@ -1230,7 +1420,8 @@ body.mod-open .mod-panel { display: block; }
|
||||
.cta-btn,
|
||||
.btn-next,
|
||||
.narrative-cta-btn,
|
||||
.capture-submit {
|
||||
.capture-submit,
|
||||
.protein-offer-btn {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background: transparent;
|
||||
@@ -1240,7 +1431,8 @@ body.mod-open .mod-panel { display: block; }
|
||||
.cta-btn::before,
|
||||
.btn-next::before,
|
||||
.narrative-cta-btn::before,
|
||||
.capture-submit::before {
|
||||
.capture-submit::before,
|
||||
.protein-offer-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -1274,7 +1466,8 @@ body.mod-open .mod-panel { display: block; }
|
||||
.cta-btn:hover,
|
||||
.btn-next:hover,
|
||||
.narrative-cta-btn:hover,
|
||||
.capture-submit:hover {
|
||||
.capture-submit:hover,
|
||||
.protein-offer-btn:hover {
|
||||
background: transparent;
|
||||
color: #fff8e8;
|
||||
transform: none;
|
||||
@@ -1283,7 +1476,172 @@ body.mod-open .mod-panel { display: block; }
|
||||
.cta-btn:active,
|
||||
.btn-next:active,
|
||||
.narrative-cta-btn:active,
|
||||
.capture-submit:active { opacity: 0.9; }
|
||||
.capture-submit:active,
|
||||
.protein-offer-btn:active { opacity: 0.9; }
|
||||
|
||||
/* Green-paint variant — the protein-offer button always wears the green
|
||||
paint regardless of body.paint-* state (parallels body.paint-green
|
||||
.cta-btn::before, but pinned to this button as a one-off). */
|
||||
.protein-offer-btn { color: #f4ecd8; }
|
||||
.protein-offer-btn::before {
|
||||
background: #2a3010;
|
||||
filter: url(#paintGlossBtn);
|
||||
-webkit-filter: url(#paintGlossBtn);
|
||||
}
|
||||
.protein-offer-btn:hover::before {
|
||||
filter: url(#paintGlossBtnHover);
|
||||
-webkit-filter: url(#paintGlossBtnHover);
|
||||
}
|
||||
|
||||
/* Unified button typography — every primary CTA wears the .cta-btn font
|
||||
(var(--font-body), 400 weight, 13px, 0.2em tracking, uppercase) so the
|
||||
results-page actions match the homepage "Begin" button. Padding and
|
||||
width stay per-button so visual hierarchy is preserved. Place after
|
||||
the per-button rules above so this wins the cascade. */
|
||||
.cta-btn,
|
||||
.btn-next,
|
||||
.btn-back,
|
||||
.narrative-cta-btn,
|
||||
.capture-submit,
|
||||
.protein-offer-btn,
|
||||
.restart-btn,
|
||||
.form-modal-close {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ── FORM MODAL ──
|
||||
Layout: fixed overlay with the panel scaled to the viewport. Panel is a
|
||||
flex column — header (sticky) / body (scrollable) / footer (sticky) —
|
||||
so the submit button stays in view while long forms scroll inside. */
|
||||
.form-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
display: none;
|
||||
padding: 16px;
|
||||
background: rgba(20, 20, 20, 0.55);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(6px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.form-modal.open {
|
||||
display: flex;
|
||||
animation: fadeUp 0.22s ease;
|
||||
}
|
||||
.form-modal-panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 580px;
|
||||
max-height: calc(100dvh - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #FAFAFA;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12px 32px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.7);
|
||||
animation: fadeUp 0.3s ease both;
|
||||
}
|
||||
.form-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.form-modal-header .modal-title {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.form-modal-body {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 18px;
|
||||
}
|
||||
.form-modal-body .capture-sub {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-modal-footer {
|
||||
flex-shrink: 0;
|
||||
padding: 14px 18px max(14px, env(safe-area-inset-bottom));
|
||||
border-top: 1px solid rgba(0,0,0,0.06);
|
||||
background: #FAFAFA;
|
||||
}
|
||||
.form-modal-footer .capture-submit {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
/* Close button — secondary style: white card with subtle border (parallels
|
||||
the .btn-back / restart-btn flat panel look). The × is drawn from two
|
||||
CSS strokes via ::before / ::after so it sits pixel-centred regardless
|
||||
of font metrics; the button's text content is sized to 0 so the inline
|
||||
"×" character isn't rendered. */
|
||||
.form-modal-close {
|
||||
position: relative;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border-radius: 4px;
|
||||
background: #FAFAFA;
|
||||
color: var(--text);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
font-size: 0; /* hides the literal "×" character */
|
||||
line-height: 0;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.7);
|
||||
}
|
||||
.form-modal-close::before,
|
||||
.form-modal-close::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 14px;
|
||||
height: 1.5px;
|
||||
background: var(--text);
|
||||
border-radius: 1px;
|
||||
transform-origin: center;
|
||||
}
|
||||
.form-modal-close::before { transform: translate(-50%, -50%) rotate(45deg); }
|
||||
.form-modal-close::after { transform: translate(-50%, -50%) rotate(-45deg); }
|
||||
.form-modal-close:hover { background: #FFFFFF; border-color: rgba(0,0,0,0.20); }
|
||||
.form-modal-close:active { background: #F0F0F0; }
|
||||
|
||||
/* In-form privacy note — sits between the protein field and newsletter
|
||||
checkbox (above the fixed footer submit). */
|
||||
.form-privacy {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
line-height: 1.7;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
/* iOS-Safari-safe scroll lock — overflow:hidden alone leaks touch scroll
|
||||
on iOS, so we also pin position:fixed and width:100%. JS sets the
|
||||
`top` inline style to -savedScrollY so the page stays where it was,
|
||||
then restores scrollY on close. */
|
||||
body.modal-open {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.btn-next:disabled,
|
||||
.btn-next:disabled:hover {
|
||||
opacity: 0.4;
|
||||
|
||||
Reference in New Issue
Block a user