diff --git a/client/src/components/FourZero/NoFearBeacon.tsx b/client/src/components/FourZero/NoFearBeacon.tsx
index d34b609d8..3e07342a0 100644
--- a/client/src/components/FourZero/NoFearBeacon.tsx
+++ b/client/src/components/FourZero/NoFearBeacon.tsx
@@ -1,13 +1,20 @@
import { lazy, Suspense, useEffect, useRef, useState } from 'react'
-import { Play } from 'lucide-react'
+import { Play, X } from 'lucide-react'
import { useTranslation } from '../../i18n'
-import { noFearCopy } from './noFearLines'
+import { noFearChrome, noFearCopy } from './noFearLines'
import './fourzero.css'
// TREK 4.0.0 release moment — remove together with the FourZero folder.
const NoFearShow = lazy(() => import('./NoFearShow'))
+// The moment retires itself. Last day it shows: 2026-08-23 (local midnight after
+// it ends the run). After that the beacon renders nothing even if the folder is
+// still in the build — so self-hosters who don't update past this version still
+// see it disappear on time. The end date holds regardless of updates.
+const VISIBLE_UNTIL = new Date('2026-08-24T00:00:00').getTime()
+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][] = [
@@ -123,7 +130,20 @@ function TeaserCanvas() {
export default function NoFearBeacon() {
const { language } = useTranslation()
const copy = noFearCopy(language)
+ const chrome = noFearChrome(language)
const [open, setOpen] = useState(false)
+ const [confirming, setConfirming] = useState(false)
+ const [dismissed, setDismissed] = useState(() => {
+ try { return localStorage.getItem(DISMISS_KEY) === '1' } catch { return false }
+ })
+
+ // Past the end date, or dismissed for good — nothing renders.
+ if (dismissed || Date.now() >= VISIBLE_UNTIL) return null
+
+ const dismiss = () => {
+ try { localStorage.setItem(DISMISS_KEY, '1') } catch { /* private mode — hide for this session at least */ }
+ setDismissed(true)
+ }
return (
<>
@@ -135,6 +155,21 @@ export default function NoFearBeacon() {
{copy.beaconSub}
+ {/* Top-right X: two-step so a stray click can't retire the moment for good.
+ Dismissing is permanent (localStorage) — only an update brings it back. */}
+
+ {/* White retirement badge: viewable until Aug 23. */}
+ {chrome.until}
{open && (
diff --git a/client/src/components/FourZero/fourzero.css b/client/src/components/FourZero/fourzero.css
index 100f300de..cd150ebc4 100644
--- a/client/src/components/FourZero/fourzero.css
+++ b/client/src/components/FourZero/fourzero.css
@@ -396,19 +396,59 @@
.fz-beacon-dismiss {
position: absolute;
- top: 8px;
- inset-inline-start: 8px;
- z-index: 2;
+ top: 10px;
+ inset-inline-end: 10px;
+ z-index: 3;
display: inline-flex;
+ align-items: center;
padding: 5px;
border: none;
border-radius: 999px;
- background: transparent;
- color: rgba(245, 240, 225, 0.4);
+ background: rgba(8, 10, 16, 0.4);
+ color: rgba(245, 240, 225, 0.55);
+ font-family: inherit;
+ font-size: 11px;
+ font-weight: 600;
cursor: pointer;
+ opacity: 0;
+ backdrop-filter: blur(4px);
+ transition: opacity 0.3s ease, color 0.2s, background 0.2s;
}
-.fz-beacon-dismiss:hover { color: rgba(245, 240, 225, 0.85); }
+/* Stays out of the way until the card is hovered or the button is focused. */
+.fz-beacon:hover .fz-beacon-dismiss,
+.fz-beacon-dismiss:focus-visible { opacity: 1; }
+
+.fz-beacon-dismiss:hover { color: #fff; background: rgba(8, 10, 16, 0.65); }
+
+/* Armed confirm: a clear, deliberate red pill — this click is permanent. */
+.fz-beacon-dismiss-confirm {
+ opacity: 1;
+ padding: 5px 11px;
+ background: rgba(200, 60, 45, 0.9);
+ color: #fff;
+}
+
+.fz-beacon-dismiss-confirm:hover { background: rgba(214, 68, 52, 0.95); }
+
+/* White retirement badge at the foot of the card: viewable until Aug 23. */
+.fz-beacon-until {
+ position: absolute;
+ left: 50%;
+ bottom: 14px;
+ transform: translateX(-50%);
+ z-index: 3;
+ pointer-events: none;
+ padding: 4px 12px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.92);
+ color: #1a1712;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.01em;
+ white-space: nowrap;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
+}
/* While the show runs: no floating dashboard chrome above it, no page scroll. */
body.fz-show-open { overflow: hidden; }
diff --git a/client/src/components/FourZero/noFearLines.ts b/client/src/components/FourZero/noFearLines.ts
index 61dbaf0cd..6092e21c4 100644
--- a/client/src/components/FourZero/noFearLines.ts
+++ b/client/src/components/FourZero/noFearLines.ts
@@ -789,3 +789,45 @@ export const ANTHEM_CASCADE: { lang: string; text: string }[] = [
]
export const INSPIRED_URL = 'https://www.youtube.com/watch?v=Gg-SCpXba64'
+
+// The beacon retires itself after the release window. The white "viewable until"
+// badge and the dismiss copy live in one compact table so the 23 language blocks
+// above stay untouched — and it all still leaves with the folder.
+interface NoFearChrome {
+ /** The white badge: this moment is viewable until Aug 23. */
+ until: string
+ /** aria-label / title of the top-right X. */
+ dismiss: string
+ /** Armed confirm label — a stray click must not retire it for good. */
+ confirm: string
+}
+
+const CHROME: Record = {
+ de: { until: 'Noch bis 23.08. zu sehen', dismiss: 'Ausblenden', confirm: 'Dauerhaft ausblenden?' },
+ en: { until: 'Shown until Aug 23', dismiss: 'Dismiss', confirm: 'Hide for good?' },
+ es: { until: 'Visible hasta el 23 ago', dismiss: 'Ocultar', confirm: '¿Ocultar para siempre?' },
+ fr: { until: 'Visible jusqu’au 23 août', dismiss: 'Masquer', confirm: 'Masquer définitivement ?' },
+ it: { until: 'Visibile fino al 23 ago', dismiss: 'Nascondi', confirm: 'Nascondere per sempre?' },
+ nl: { until: 'Te zien tot 23 aug', dismiss: 'Verbergen', confirm: 'Voorgoed verbergen?' },
+ pl: { until: 'Widoczne do 23 sierpnia', dismiss: 'Ukryj', confirm: 'Ukryć na zawsze?' },
+ ru: { until: 'Показывается до 23 августа', dismiss: 'Скрыть', confirm: 'Скрыть навсегда?' },
+ uk: { until: 'Доступно до 23 серпня', dismiss: 'Сховати', confirm: 'Сховати назавжди?' },
+ cs: { until: 'K vidění do 23. srpna', dismiss: 'Skrýt', confirm: 'Skrýt natrvalo?' },
+ sv: { until: 'Visas till 23 aug', dismiss: 'Dölj', confirm: 'Dölj för gott?' },
+ tr: { until: '23 Ağustos’a kadar görünür', dismiss: 'Gizle', confirm: 'Kalıcı olarak gizlensin mi?' },
+ gr: { until: 'Έως 23 Αυγούστου', dismiss: 'Απόκρυψη', confirm: 'Μόνιμη απόκρυψη;' },
+ hu: { until: 'Augusztus 23-ig látható', dismiss: 'Elrejtés', confirm: 'Végleg elrejti?' },
+ br: { until: 'Visível até 23 de ago', dismiss: 'Ocultar', confirm: 'Ocultar para sempre?' },
+ ca: { until: 'Visible fins al 23 d’ag.', dismiss: 'Amaga', confirm: 'Amagar per sempre?' },
+ id: { until: 'Tampil hingga 23 Agu', dismiss: 'Sembunyikan', confirm: 'Sembunyikan selamanya?' },
+ vi: { until: 'Hiển thị đến 23 tháng 8', dismiss: 'Ẩn', confirm: 'Ẩn vĩnh viễn?' },
+ ja: { until: '8月23日まで公開', dismiss: '非表示', confirm: '完全に非表示にしますか?' },
+ ko: { until: '8월 23일까지 표시', dismiss: '숨기기', confirm: '영구히 숨기시겠어요?' },
+ zh: { until: '展示至 8月23日', dismiss: '隐藏', confirm: '永久隐藏?' },
+ ar: { until: 'يظهر حتى 23 أغسطس', dismiss: 'إخفاء', confirm: 'إخفاء نهائيًا؟' },
+ 'zh-TW': { until: '展示至 8月23日', dismiss: '隱藏', confirm: '永久隱藏?' },
+}
+
+export function noFearChrome(language: string): NoFearChrome {
+ return CHROME[language] ?? CHROME.en
+}