From 068d333c8aa930ca815203d51ca7744042c7c630 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 10 May 2026 15:26:07 +0100 Subject: [PATCH] intro updates --- public/icons/cloud-lightning-icon.svg | 1 + public/icons/hand-watch-icon.svg | 1 + public/icons/secure-icon.svg | 1 + src/App.vue | 69 +++++- src/styles.css | 298 ++++++++++++++++++++++---- 5 files changed, 325 insertions(+), 45 deletions(-) create mode 100644 public/icons/cloud-lightning-icon.svg create mode 100644 public/icons/hand-watch-icon.svg create mode 100644 public/icons/secure-icon.svg diff --git a/public/icons/cloud-lightning-icon.svg b/public/icons/cloud-lightning-icon.svg new file mode 100644 index 0000000..8ae6e30 --- /dev/null +++ b/public/icons/cloud-lightning-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/hand-watch-icon.svg b/public/icons/hand-watch-icon.svg new file mode 100644 index 0000000..d55fecc --- /dev/null +++ b/public/icons/hand-watch-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/secure-icon.svg b/public/icons/secure-icon.svg new file mode 100644 index 0000000..0b4ca08 --- /dev/null +++ b/public/icons/secure-icon.svg @@ -0,0 +1 @@ +secure \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 495ffa8..927d741 100644 --- a/src/App.vue +++ b/src/App.vue @@ -228,13 +228,44 @@ loads in the same tab via sessionStorage. --> @@ -613,8 +644,8 @@ const T = { hero_sub: "Preparedness, refined.", hero_cta: "Begin", intro_l1: "Wisdom is to prepare", - intro_l2: "Even if crisis is not here yet", - intro_l3: "Figure out your Plan B in less than two minutes.", + intro_l2: "Even if\ncrisis is not here yet", + intro_l3: "Figure out your Plan B in less than 2 Minutes.", stat_scenarios: "Scenarios", stat_questions: "Questions", stat_free: "Free Forever", @@ -733,8 +764,8 @@ const T = { hero_sub: "Vorsorge, verfeinert.", hero_cta: "Beginnen", intro_l1: "Weise ist, wer vorsorgt.", - intro_l2: "Auch wenn noch keine Krise da ist.", - intro_l3: "Finden Sie Ihren Plan B in weniger als zwei Minuten.", + intro_l2: "Auch wenn\nnoch keine Krise da ist.", + intro_l3: "Finden Sie Ihren Plan B in weniger als 2 Minuten.", stat_scenarios: "Szenarien", stat_questions: "Fragen", stat_free: "Kostenlos", @@ -1861,17 +1892,39 @@ async function playIntro() { const text = textEl.textContent.trim() textEl.innerHTML = '' words = text.split(/\s+/) + // Per-stage emphasis. Stage 1 bolds "prepare" / "vorsorgt"; stage + // 2 bolds "crisis" / "krise"; stage 3 bolds the final two words + // ("2 Minutes." / "2 Minuten.") — both EN and DE land at the + // same word index. Word matching is case-insensitive and trailing + // punctuation is stripped before testing. + const emphasisRe = { + 1: /^(prepare|vorsorgt)$/i, + 2: /^(crisis|krise)$/i, + }[n] || null + const stripPunct = w => w.replace(/[.,;:!?]+$/, '') + // Stage 3 — bold "less than 2 Minutes." (last four words). + const boldFromIdx = (n === 3) ? 6 : Infinity words.forEach((word, i) => { const span = document.createElement('span') - span.className = 'word' + const isBold = i >= boldFromIdx || (emphasisRe && emphasisRe.test(stripPunct(word))) + span.className = 'word' + (isBold ? ' bold' : '') span.style.animationDelay = (BASE + i * STAGGER) + 's' span.textContent = word textEl.appendChild(span) + // Stage 2 — insert a mobile-only line break after the leading + // conjunction so "crisis is not here yet" / "noch keine Krise + // da ist." sits on its own row on small viewports. Hidden on + // desktop via CSS. + if (n === 2 && /^(if|wenn)$/i.test(stripPunct(word))) { + textEl.appendChild(document.createElement('br')) + .className = 'mobile-break' + } }) } const entryMs = Math.round((BASE + (Math.max(0, words.length - 1)) * STAGGER + PER_WORD) * 1000) - // Stage 3 holds longest because it's the longest line. - const hold = n === 3 ? 1500 : 700 + // Bigger hold on stage 1 so the opening line lingers; stage 3 (the + // longest sentence) holds the longest. + const hold = n === 3 ? 1500 : (n === 1 ? 1200 : 900) stages.push({ sel: '.stage-' + n, enter: entryMs, hold }) } const exit = 800 // matches the introOut (smoke out) duration diff --git a/src/styles.css b/src/styles.css index 8508698..1fe3c1c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -90,7 +90,11 @@ body { background: #FAFAFA; color: var(--text); font-family: var(--font-body); f .lang-btn.active { background: var(--red); color: #FFFFFF; } /* ── APP ── */ -.app { padding-top: 0; height: 100vh; height: 100dvh; display: flex; flex-direction: column; overflow: hidden; } +.app { padding-top: 0; height: 100vh; height: 100dvh; display: flex; flex-direction: column; overflow: hidden; position: relative; isolation: isolate; } +.app > :not(.page-bg-pattern) { + position: relative; + z-index: 1; +} /* ── HERO ── */ .hero { @@ -117,21 +121,13 @@ body { background: #FAFAFA; color: var(--text); font-family: var(--font-body); f .page-bg-pattern { position: fixed; inset: 0; - /* z-index:-1 keeps the pattern below normal-flow content (hero / quiz - / results) without forcing those sections into their own stacking - context. That matters because creating a context on .results-section - would trap the capture-modal (a position:fixed descendant) inside - it, and its z-index:200 wouldn't escape that context to sit above - the body-level site-header. Body bg is the canvas; the pattern - paints above the canvas and below positioned content. */ - z-index: -1; + z-index: 0; pointer-events: none; user-select: none; overflow: hidden; background: #FAFAFA; - /* Parent is always visible — the intro overlay covers it during the - intro, and the per-row animations handle the staggered reveal once - intro-done lands. Nothing to fade on the wrapper. */ + /* Parent is always visible. The intro overlay is translucent, so the + animated emboss pattern is already present behind the first line. */ opacity: 1; } .page-bg-pattern .bg-tilt { @@ -178,28 +174,59 @@ body { background: #FAFAFA; color: var(--text); font-family: var(--font-body); f sits inside the rotated .bg-tilt, so the slide is slightly off-axis in screen space (matches the diagonal tilt). */ .page-bg-pattern .bg-row { + --row-delay: 0s; opacity: 0; will-change: opacity, transform; } -.page-bg-pattern .bg-row:nth-child(odd) { transform: translateX(-160px); } -.page-bg-pattern .bg-row:nth-child(even) { transform: translateX( 160px); } +.page-bg-pattern .bg-row:nth-child(odd) { + transform: translateX(-160px); + animation: + bgRowSlideLeft 1.1s var(--row-delay) cubic-bezier(0.22, 0.61, 0.36, 1) forwards, + bgRowDriftLeft 14s calc(var(--row-delay) + 2.1s) ease-in-out infinite; +} +.page-bg-pattern .bg-row:nth-child(even) { + transform: translateX(160px); + animation: + bgRowSlideRight 1.1s var(--row-delay) cubic-bezier(0.22, 0.61, 0.36, 1) forwards, + bgRowDriftRight 14s calc(var(--row-delay) + 2.1s) ease-in-out infinite; +} +body:not(.intro-done) .page-bg-pattern .bg-row { + opacity: 1; +} +body:not(.intro-done) .page-bg-pattern .bg-row:nth-child(odd) { + animation: bgRowDriftLeft 14s ease-in-out infinite; +} +body:not(.intro-done) .page-bg-pattern .bg-row:nth-child(even) { + animation: bgRowDriftRight 14s ease-in-out infinite; +} +body:not(.intro-done) .page-bg-pattern .bg-row { + text-shadow: + -1px -1px 0 rgba(0, 0, 0, 0.075), + -2px -2px 1px rgba(0, 0, 0, 0.035), + 1px 1px 0 rgba(255, 255, 255, 0.9); +} -body.intro-done .page-bg-pattern .bg-row:nth-child(odd) { - animation: bgRowSlideLeft 1.1s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; +/* Simple slide-in per row, alternating direction, followed by a perpetual + alternating drift. This starts immediately so the pattern is visible + underneath the intro overlay from the first sentence onward. */ +@keyframes bgRowDriftLeft { + 0%, 100% { transform: translateX(0); } + 50% { transform: translateX(-60px); } } -body.intro-done .page-bg-pattern .bg-row:nth-child(even) { - animation: bgRowSlideRight 1.1s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; +@keyframes bgRowDriftRight { + 0%, 100% { transform: translateX(0); } + 50% { transform: translateX(60px); } } -body.intro-done .page-bg-pattern .bg-row:nth-child(1) { animation-delay: 0.00s; } -body.intro-done .page-bg-pattern .bg-row:nth-child(2) { animation-delay: 0.10s; } -body.intro-done .page-bg-pattern .bg-row:nth-child(3) { animation-delay: 0.20s; } -body.intro-done .page-bg-pattern .bg-row:nth-child(4) { animation-delay: 0.30s; } -body.intro-done .page-bg-pattern .bg-row:nth-child(5) { animation-delay: 0.40s; } -body.intro-done .page-bg-pattern .bg-row:nth-child(6) { animation-delay: 0.50s; } -body.intro-done .page-bg-pattern .bg-row:nth-child(7) { animation-delay: 0.60s; } -body.intro-done .page-bg-pattern .bg-row:nth-child(8) { animation-delay: 0.70s; } -body.intro-done .page-bg-pattern .bg-row:nth-child(9) { animation-delay: 0.80s; } -body.intro-done .page-bg-pattern .bg-row:nth-child(10) { animation-delay: 0.90s; } +.page-bg-pattern .bg-row:nth-child(1) { --row-delay: 0.00s; } +.page-bg-pattern .bg-row:nth-child(2) { --row-delay: 0.10s; } +.page-bg-pattern .bg-row:nth-child(3) { --row-delay: 0.20s; } +.page-bg-pattern .bg-row:nth-child(4) { --row-delay: 0.30s; } +.page-bg-pattern .bg-row:nth-child(5) { --row-delay: 0.40s; } +.page-bg-pattern .bg-row:nth-child(6) { --row-delay: 0.50s; } +.page-bg-pattern .bg-row:nth-child(7) { --row-delay: 0.60s; } +.page-bg-pattern .bg-row:nth-child(8) { --row-delay: 0.70s; } +.page-bg-pattern .bg-row:nth-child(9) { --row-delay: 0.80s; } +.page-bg-pattern .bg-row:nth-child(10) { --row-delay: 0.90s; } @keyframes bgRowSlideLeft { from { opacity: 0; transform: translateX(-160px); } to { opacity: 1; transform: translateX(0); } @@ -211,10 +238,7 @@ body.intro-done .page-bg-pattern .bg-row:nth-child(10) { animation-delay: 0.90s; /* (Per-page bg override removed — every page now uses #FAFAFA.) */ -/* No section-level stacking context needed — the pattern lives at - z-index:-1 so normal-flow sections paint above it automatically. - Avoiding a stacking context on .results-section also lets the - capture-modal (z-index:200) reach above the site-header. */ +/* Hero copy sits above the fixed pattern layer. */ .hero-eyebrow { position: relative; z-index: 1; font-family: var(--font-body); @@ -345,7 +369,7 @@ html[lang="en"] .hero h1 { font-size: calc((100vw - 32px) / 5.2); } max-width: 520px; margin: 0 auto 40px; line-height: 1.6; - animation: fadeUp 1s 1.3s ease both; + animation: fadeUp 1s 2.0s ease both; } .cta-btn { display: inline-flex; @@ -360,7 +384,7 @@ html[lang="en"] .hero h1 { font-size: calc((100vw - 32px) / 5.2); } border-radius: 0; cursor: pointer; transition: color 0.3s ease, var(--trans); - animation: fadeUp 1s 1.45s ease both; + animation: fadeUp 1s 2.45s ease both; /* Lift above .hero::after radial fade so the button isn't washed out */ z-index: 2; } @@ -782,14 +806,26 @@ input[type=range]::-moz-range-thumb { to dark for legibility on the light background. */ .rec-cards { animation: fadeUp 0.4s 0.15s ease both; } .rec-card { - background: #F0F0F0; + /* Lighter card body — was #F0F0F0 (read as grey on the page); now + #FAFAFA so the product info sits on a near-white surface. The + header above takes the light sage tone as a visual section break. */ + background: #FAFAFA; 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); } +/* Header strip wears the same light sage as the .rp-hint banner so + the rec-card sections read as a coordinated family. */ +.rec-header { + padding: 14px 16px; + display: flex; + align-items: center; + gap: 12px; + background: #E5F0E0; + border-bottom: 1px solid rgba(90,154,120,0.22); +} /* Green-paint icon chip — matches the painted CTA buttons. ::before carries the dark-green fill with the gloss filter; the icon SVG inside inherits the warm-cream stroke colour via currentColor. */ @@ -1143,7 +1179,7 @@ input[type=range]::-moz-range-thumb { position: fixed; inset: 0; z-index: 1000; - background: #FAFAFA; + background: transparent; display: flex; align-items: center; justify-content: center; @@ -1157,12 +1193,177 @@ input[type=range]::-moz-range-thumb { position: absolute; inset: 0; display: flex; + flex-direction: column; align-items: center; justify-content: center; + gap: 28px; text-align: center; padding: 0 32px; white-space: pre-line; } + +/* Stage icons — paint-style badge that visually persists across all + three stages (same circle, same paint filter, same position) so the + inner glyph appears to morph through it as stages crossfade. The + glyph itself draws in stroke-by-stroke per stage. Cream stroke on + dark green paint matches the CTA buttons. */ +.stage-icon { + display: block; + width: clamp(72px, 13vw, 100px); + height: clamp(72px, 13vw, 100px); + fill: none; + opacity: 0; + transform: scale(0.78); + transform-origin: center; +} +.stage-icon .badge { + fill: #2a3010; + stroke: none; + filter: url(#paintGlossBtn); + -webkit-filter: url(#paintGlossBtn); +} +.stage-icon .glyph { + color: #f4ecd8; + stroke: currentColor; + /* Slightly thicker strokes so the cream icons read with confidence + against the dark green paint chip. */ + stroke-width: 3.2; + fill: none; + stroke-linecap: round; + stroke-linejoin: round; +} +/* Per-element overrides — accent strokes (ridge, hour hand, side + buttons) are thinner so they read as detail rather than primary. */ +.stage-icon .glyph .i-ridge { stroke-width: 1.4; opacity: 0.55; } +.stage-icon .glyph .i-side { stroke-width: 2.2; } +.stage-icon .glyph .i-hour { stroke-width: 2.6; } +.stage-icon .glyph .i-tick { stroke-width: 2.4; } +.stage-icon .glyph .i-pin { fill: currentColor; stroke: none; } +/* Bolder mark / bolt — the punctuation strokes inside each icon. */ +.stage-icon .glyph .i-mark { stroke-width: 4.2; } +.stage-icon .glyph .i-bolt { stroke-width: 4.0; } +/* Glyph paths/lines/circles draw in stroke-by-stroke. pathLength: 1 + normalises the dashoffset so each piece draws over the same time. */ +.stage-icon .glyph path, +.stage-icon .glyph line, +.stage-icon .glyph circle { + stroke-dasharray: 1; + stroke-dashoffset: 1; + pathLength: 1; +} +.intro-stage.active .stage-icon { + opacity: 1; + transform: scale(1); + transition: + opacity 0.25s ease 0.05s, + transform 0.55s cubic-bezier(0.18, 0.89, 0.32, 1.18) 0.05s; +} +.intro-stage.active .stage-icon .glyph path, +.intro-stage.active .stage-icon .glyph line, +.intro-stage.active .stage-icon .glyph circle { + animation: introDraw 0.9s 0.1s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; +} +@keyframes introDraw { + to { stroke-dashoffset: 0; } +} +.stage-icon .filled-icon path { + fill: currentColor; + stroke: none; + opacity: 0; + transform: scale(0.84); + transform-box: fill-box; + transform-origin: center; +} +.intro-stage.active .stage-icon .filled-icon path { + animation: introGlyphPop 0.65s 0.18s cubic-bezier(0.18, 0.89, 0.32, 1.18) forwards; +} +@keyframes introGlyphPop { + to { opacity: 1; transform: scale(1); } +} +.stage-icon .glyph-stroke { + fill: none; + stroke: #fff8dc; + stroke-width: 4.2; + stroke-linecap: round; + stroke-linejoin: round; + stroke-dasharray: 1; + stroke-dashoffset: 1; + pathLength: 1; + opacity: 0; + filter: drop-shadow(0 0 5px rgba(255, 248, 220, 0.55)); +} +.stage-icon .i-shield-outline { + stroke-width: 3.8; +} +.stage-icon .glyph-fill { + fill: #fff8dc; + stroke: none; + opacity: 0; + filter: + drop-shadow(0 0 5px rgba(255, 248, 220, 0.95)) + drop-shadow(0 0 14px rgba(244, 236, 216, 0.7)); +} + +/* Stage 1 — the shield border appears first; the filled check ticks in + near the end of the sentence, after "prepare" has landed. */ +.intro-stage.stage-1.active .i-shield-outline { + animation: introDraw 0.78s 0.18s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; + opacity: 1; +} +.intro-stage.stage-1.active .i-secure-tick { + animation: tickFillIn 0.48s 2.35s cubic-bezier(0.18, 0.89, 0.32, 1.18) forwards; + transform-box: fill-box; + transform-origin: center; +} +@keyframes tickFillIn { + 0% { opacity: 0; transform: scale(0.55) rotate(-8deg); } + 65% { opacity: 1; transform: scale(1.13) rotate(0deg); } + 100% { opacity: 1; transform: scale(1) rotate(0deg); } +} + +/* Stage 2 — the supplied cloud icon appears first; the bolt flashes like + lightning once the crisis sentence is readable. */ +.intro-stage.stage-2.active .i-lightning-flash { + animation: lightningStrike 1.08s 0.45s steps(1, end) infinite; +} +@keyframes lightningStrike { + 0% { opacity: 0; } + 7% { opacity: 1; } + 12% { opacity: 0.16; } + 18% { opacity: 1; } + 25% { opacity: 0; } + 42% { opacity: 0.92; } + 50% { opacity: 0.18; } + 64% { opacity: 1; } + 72% { opacity: 0; } + 100% { opacity: 0; } +} + +/* Stage 3 — face + ticks draw, then the minute hand takes the sentence + timing to sweep two full rotations and land back at 12. */ +.icon-clock .i-crown { animation-delay: 0.10s !important; } +.icon-clock .i-face { animation-delay: 0.20s !important; animation-duration: 0.7s !important; } +.icon-clock .i-tick { animation-delay: 0.70s !important; animation-duration: 0.3s !important; } +.icon-clock .i-hand { + transform-origin: 48px 48px; + animation-delay: 0.70s !important; + animation-duration: 0.35s !important; +} +.intro-stage.stage-3.active .icon-clock .i-hand { + animation: introDraw 0.35s 0.70s cubic-bezier(0.22, 0.61, 0.36, 1) forwards, + clockSweep 2.2s 1.35s linear forwards; +} +@keyframes clockSweep { + from { transform: rotate(0deg); } + to { transform: rotate(720deg); } +} + +/* Stage exit — keep the badge/icon present while text dissolves, so the + circle feels continuous instead of outroing between sentences. */ +.intro-stage.leaving .stage-icon { + opacity: 1; + transform: scale(1); +} /* Intro text — uppercase Barlow at heading scale with wide tracking, matching the "Preparedness. Refined." sub-line typeface. No paint filter / no gradient text-fill (those tanked render performance during @@ -1201,6 +1402,29 @@ input[type=range]::-moz-range-thumb { was the lag source. */ will-change: opacity, transform; } +/* Per-stage emphasis — JS adds .bold to specific words per intro line + ("prepare" / "vorsorgt", "crisis" / "krise", and the closing clause + on stage 3). */ +.intro-stage .intro-text > .word.bold { + font-weight: 700; +} + +/* Stage 2 mobile line break — JS inserts
+ after the leading conjunction ("if" / "wenn") so the second clause + ("crisis is not here yet" / "noch keine Krise da ist.") wraps onto + its own line on small viewports. Hidden on desktop via display:none + so the sentence renders on a single line. */ +.intro-stage br.mobile-break { display: inline; } +@media (min-width: 768px) { + .intro-stage br.mobile-break { display: none; } +} +/* Mobile-only line break — JS inserts a
+ after "if" / "wenn" on stage 2. Display:none on wider viewports so + the line stays one row on desktop / tablet. */ +.intro-stage .intro-text .mobile-break { display: inline; } +@media (min-width: 768px) { + .intro-stage .intro-text .mobile-break { display: none; } +} .intro-stage.active .intro-text > .word { animation: introWord 1.05s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; }