From ffda4de77d5d9fe77eaa4ce31d5966e14a964fa3 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 24 Jul 2026 00:04:08 +0200 Subject: [PATCH] feat(dashboard): the anthem IS the ending + beacon final polish + navbar fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The show now ends standing: KEINE ANGST with the full 23-language cascade stays up over the lit world, music carrying on until the traveler closes it — no credits hand-off. Skip jumps straight to that standing frame. Beacon: dismiss × removed, copy block centered on the canvas, the title gets its quiet jewelry treatment — ivory-to-gold fill, a traveling sheen and a slow breathing glow. The overlay now portals to document.body: the sticky dashboard sidebar forms its own stacking context, so the fixed navbar (z-200 at root) used to float above the show regardless of the overlay's z-index. --- .../src/components/FourZero/NoFearBeacon.tsx | 18 +-- client/src/components/FourZero/NoFearShow.tsx | 38 +++--- client/src/components/FourZero/fourzero.css | 109 ++++-------------- 3 files changed, 40 insertions(+), 125 deletions(-) diff --git a/client/src/components/FourZero/NoFearBeacon.tsx b/client/src/components/FourZero/NoFearBeacon.tsx index 889006f41..d34b609d8 100644 --- a/client/src/components/FourZero/NoFearBeacon.tsx +++ b/client/src/components/FourZero/NoFearBeacon.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense, useEffect, useRef, useState } from 'react' -import { Play, X } from 'lucide-react' +import { Play } from 'lucide-react' import { useTranslation } from '../../i18n' import { noFearCopy } from './noFearLines' import './fourzero.css' @@ -8,8 +8,6 @@ import './fourzero.css' const NoFearShow = lazy(() => import('./NoFearShow')) -const DISMISS_KEY = 'trek-fourzero-dismissed' - // Abstract night-world for the teaser: light points loosely shaped like the // inhabited continents (relative coords), between which golden arcs travel. const TEASER_POINTS: [number, number][] = [ @@ -120,17 +118,13 @@ function TeaserCanvas() { /** * The dashboard trigger for the 4.0.0 "KEINE ANGST" show: a dark cinematic card * with a living web of golden travel arcs (desktop only). One click starts the - * show; the small × dismisses the beacon for good on this device. - * Core colors ride inline so no dashboard widget CSS can wash them out. + * show. Core colors ride inline so no dashboard widget CSS can wash them out. */ export default function NoFearBeacon() { const { language } = useTranslation() const copy = noFearCopy(language) - const [dismissed, setDismissed] = useState(() => localStorage.getItem(DISMISS_KEY) === '1') const [open, setOpen] = useState(false) - if (dismissed) return null - return ( <>
@@ -141,14 +135,6 @@ export default function NoFearBeacon() { {copy.beaconSub} -
{open && ( diff --git a/client/src/components/FourZero/NoFearShow.tsx b/client/src/components/FourZero/NoFearShow.tsx index 9e5d2a1c1..bf968dbbe 100644 --- a/client/src/components/FourZero/NoFearShow.tsx +++ b/client/src/components/FourZero/NoFearShow.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import ReactDOM from 'react-dom' import { Volume2, VolumeX, X } from 'lucide-react' import { placesApi, tripsApi } from '../../api/client' import apiClient from '../../api/client' @@ -38,10 +39,8 @@ const CUES: Cue[] = [ { at: 58.8, line: 'yourStatsCountries' }, { at: 64.0, line: 'notAnOpinion', hard: true, impact: 1 }, { at: 71.0, act: 'anthem', line: null, impact: 0.8 }, - { at: 84.0, act: 'end', line: null }, ] const ANTHEM_AT = 71 -const CREDITS_AT = 84 // The three staccato words strobe with the late fear act, just before the cut. const FLASH_WINDOWS: [number, number][] = [[22.2, 22.85], [23.6, 24.25], [25.0, 25.65]] @@ -62,7 +61,7 @@ function sceneAt(t: number): SceneState { warmth: ramp(t, 34, 46), personalGlow: ramp(t, 55.5, 60.5), particles: ramp(t, ANTHEM_AT, ANTHEM_AT + 4), - opacity: (1 - blackout) * (t >= CREDITS_AT ? 0.35 : 1), + opacity: 1 - blackout, } } @@ -83,15 +82,13 @@ export default function NoFearShow({ onClose }: { onClose: () => void }) { const [cueIdx, setCueIdx] = useState(-1) const [flashIdx, setFlashIdx] = useState(-1) const [muted, setMuted] = useState(false) - const [credits, setCredits] = useState(false) const [anthem, setAnthem] = useState(false) const reducedMotion = useMemo(() => window.matchMedia('(prefers-reduced-motion: reduce)').matches, []) const skipToEnd = useCallback(() => { - startRef.current = performance.now() - CREDITS_AT * 1000 + startRef.current = performance.now() - ANTHEM_AT * 1000 setAnthem(true) - setCredits(true) - audioRef.current?.setAct('end') + audioRef.current?.setAct('anthem') }, []) // Boot: audio, scene, personal places, chrome side effects. @@ -175,7 +172,7 @@ export default function NoFearShow({ onClose }: { onClose: () => void }) { const ctx = cv?.getContext('2d') if (reducedMotion) { setCueIdx(CUES.length - 1) - if (ctx) sceneRef.current?.draw(ctx, { ...sceneAt(CREDITS_AT), particles: 0 }, CREDITS_AT) + if (ctx) sceneRef.current?.draw(ctx, { ...sceneAt(ANTHEM_AT + 8), particles: 0 }, ANTHEM_AT + 8) return } let raf = 0 @@ -193,7 +190,6 @@ export default function NoFearShow({ onClose }: { onClose: () => void }) { if (cue.swell) audioRef.current?.swell() setCueIdx(idx) if (t >= ANTHEM_AT) setAnthem(true) - if (t >= CREDITS_AT) setCredits(true) } let flash = -1 for (let i = 0; i < FLASH_WINDOWS.length; i++) { @@ -228,7 +224,10 @@ export default function NoFearShow({ onClose }: { onClose: () => void }) { else if (lineKey) lineText = copy.lines[lineKey] const hard = !!cue?.hard - return ( + // Portal to body: the dashboard sidebar is position:sticky and forms its own + // stacking context, so an in-place overlay would sit UNDER the fixed navbar + // (z-200 at root level) no matter its z-index. + return ReactDOM.createPortal(
@@ -249,8 +248,8 @@ export default function NoFearShow({ onClose }: { onClose: () => void }) {
)} - {/* Anthem: the big words + the cascade through every TREK language. */} - {anthem && !credits && ( + {/* The ending: the big words + the cascade through every TREK language — and it stays. */} + {anthem && (

{copy.anthem}

@@ -263,16 +262,6 @@ export default function NoFearShow({ onClose }: { onClose: () => void }) {
)} - {/* Credits — stays until dismissed. */} - {credits && ( -
-

{copy.anthem}

-

{copy.tooBeautiful}

-

{copy.creditTitle}

-

{copy.creditBody}

-
- )} - {/* Chrome: mute, skip, close. */}
- {!credits && ( + {!anthem && ( @@ -292,6 +281,7 @@ export default function NoFearShow({ onClose }: { onClose: () => void }) {
-
+ , + document.body ) } diff --git a/client/src/components/FourZero/fourzero.css b/client/src/components/FourZero/fourzero.css index 2d22aa996..97eea64df 100644 --- a/client/src/components/FourZero/fourzero.css +++ b/client/src/components/FourZero/fourzero.css @@ -170,76 +170,6 @@ to { opacity: 0.92; transform: translateY(0); } } -/* ── credits ── */ - -.fz-credits { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 14px; - padding: 0 8vw; - text-align: center; - animation: fzLineIn 1.8s ease-out both; -} - -.fz-credit-lead { - font-size: clamp(16px, 1.7vw, 23px); - font-weight: 300; - font-style: italic; - opacity: 0.95; - margin: 6px 0 4px; - max-width: 680px; -} - -.fz-credit-title { - font-size: clamp(17px, 1.8vw, 24px); - font-weight: 700; - margin: 10px 0 0; -} - -.fz-credit-body { - font-size: clamp(14px, 1.4vw, 18px); - font-weight: 300; - opacity: 0.85; - margin: 0; - max-width: 640px; -} - -.fz-credit-link { - font-size: 13px; - color: rgba(255, 200, 120, 0.9); - text-decoration: none; - border-bottom: 1px solid rgba(255, 200, 120, 0.4); - padding-bottom: 1px; - margin-top: 6px; - pointer-events: auto; -} - -.fz-credit-link:hover { color: #ffd58a; border-color: #ffd58a; } - -.fz-close-btn { - margin-top: 26px; - padding: 11px 34px; - border-radius: 999px; - border: 1px solid rgba(245, 240, 225, 0.35); - background: transparent; - color: #f5f2ea; - font-size: 14px; - font-weight: 600; - font-family: inherit; - letter-spacing: 0.04em; - cursor: pointer; - transition: background 0.2s, border-color 0.2s; -} - -.fz-close-btn:hover { - background: rgba(245, 240, 225, 0.12); - border-color: rgba(245, 240, 225, 0.7); -} - /* ── chrome ── */ .fz-chrome { @@ -332,41 +262,50 @@ display: flex; flex-direction: column; align-items: center; - justify-content: flex-end; + justify-content: center; width: 100%; min-height: 216px; border: none; text-align: center; cursor: pointer; - padding: 64px 18px 20px; + padding: 22px 18px 18px; font-family: inherit; color: #f5f2ea; - /* Cinema scrim: darkness pools at the bottom and in the corners. */ + /* Cinema scrim: a dark pool behind the centered copy, corners vignetted. */ background: - radial-gradient(130% 90% at 50% 0%, rgba(7, 8, 13, 0) 40%, rgba(7, 8, 13, 0.5) 100%), - linear-gradient(180deg, rgba(7, 8, 13, 0) 20%, rgba(7, 8, 13, 0.62) 58%, rgba(7, 8, 13, 0.95) 100%) !important; + radial-gradient(120% 105% at 50% 52%, rgba(7, 8, 13, 0.62) 0%, rgba(7, 8, 13, 0.28) 52%, rgba(7, 8, 13, 0.7) 100%) !important; } +/* Ivory-to-gold text with a traveling sheen and a slow breathing glow — + quiet effects, jewelry-grade, no gimmicks. */ .fz-beacon-title { display: block; - font-size: 30px; + font-size: 31px; font-weight: 900; - letter-spacing: 0.13em; - text-indent: 0.13em; + letter-spacing: 0.14em; + text-indent: 0.14em; line-height: 1.04; - margin: 0 0 7px; - background: linear-gradient(100deg, #f7f3e8 0%, #f7f3e8 38%, #ffd98f 50%, #f7f3e8 62%, #f7f3e8 100%); - background-size: 240% 100%; + margin: 0 0 8px; + background-image: + linear-gradient(110deg, transparent 40%, rgba(255, 244, 214, 0.95) 50%, transparent 60%), + linear-gradient(180deg, #fdfaf1 0%, #f4e6c2 52%, #e5c084 100%); + background-size: 260% 100%, 100% 100%; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; - animation: fzTitleSheen 4.5s ease-in-out infinite; - filter: drop-shadow(0 0 22px rgba(255, 205, 130, 0.28)) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.8)); + animation: + fzTitleSheen 5.5s ease-in-out infinite, + fzTitleBreathe 4.5s ease-in-out infinite; } @keyframes fzTitleSheen { - 0%, 25% { background-position: 120% 0; } - 60%, 100% { background-position: -60% 0; } + 0%, 30% { background-position: 130% 0, 0 0; } + 62%, 100% { background-position: -70% 0, 0 0; } +} + +@keyframes fzTitleBreathe { + 0%, 100% { filter: drop-shadow(0 0 16px rgba(255, 205, 130, 0.18)) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.85)); } + 50% { filter: drop-shadow(0 0 30px rgba(255, 210, 140, 0.42)) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.85)); } } .fz-beacon-sub {