Archive
-
+
Signals from retired scanner experiments
(rsi_reversal, sma_reclaim, breakout, test/phase1). Read-only —
the bot no longer acts on any of these.
@@ -71,7 +101,7 @@ export default function ArchivePageClient() {
- {[['all', archivePosts.length] as [string, number], ...sources].map(([s, n]) => (
+ {sourceTabs.map(([s, n]) => (
{ setSrc(s); setArchivePage(1) }}
@@ -87,30 +117,30 @@ export default function ArchivePageClient() {
))}
- {loading &&
{isZh ? '加载中…' : 'Loading…'}
}
+ {loading &&
Loading…
}
{!loading && loadErr && (
- ⚠️ {isZh ? `无法加载归档:${loadErr}` : `Couldn't load archive — ${loadErr}`}
+ ⚠️ {`Couldn't load archive — ${loadErr}`}
location.reload()}>{isZh ? '重试' : 'Retry'}
+ onClick={() => location.reload()}>Retry
)}
- {!loading && !loadErr && filtered.length === 0 && (
+ {!loading && !loadErr && totalPosts === 0 && (
- {isZh ? '没有可显示的历史信号。' : 'No archived signals.'}
+ No archived signals.
)}
- {!loading && archivePageItems.length > 0 && (
+ {!loading && posts.length > 0 && (
<>
- {archivePageItems.map(p =>
)}
+ {posts.map(p =>
)}
diff --git a/app/[locale]/archive/page.tsx b/app/[locale]/archive/page.tsx
index bc13b28..a1764ff 100644
--- a/app/[locale]/archive/page.tsx
+++ b/app/[locale]/archive/page.tsx
@@ -1,13 +1,25 @@
// Archive is legacy/test data only — not part of the live signal stack.
// noindex keeps it out of search results (duplicate/thin content risk)
// while keeping it accessible to logged-in users for inspection.
+export const revalidate = 60 // archive data rarely changes — ISR at 60s keeps server load low
import type { Metadata } from 'next'
+import { type PostListResponse } from '@/lib/api'
+import { getPosts } from '@/lib/api'
+import { buildArchiveFallbackResponse, getInitialPostPage } from '@/lib/postPage'
import ArchivePageClient from './ArchivePageClient'
export const metadata: Metadata = {
robots: { index: false, follow: false },
}
-export default function ArchivePage() {
- return
+export default async function ArchivePage() {
+ const initialData: PostListResponse | null = await getInitialPostPage(30, 1, {
+ filters: { archiveOnly: true },
+ legacyFallback: async () => {
+ const legacyItems = await getPosts(500, 1).catch(() => null)
+ return legacyItems ? buildArchiveFallbackResponse(legacyItems, 1, 30) : null
+ },
+ })
+
+ return
}
diff --git a/app/[locale]/btc/page.tsx b/app/[locale]/btc/page.tsx
index 216ec77..e94df49 100644
--- a/app/[locale]/btc/page.tsx
+++ b/app/[locale]/btc/page.tsx
@@ -1,4 +1,4 @@
-import { redirect } from 'next/navigation'
+import { permanentRedirect } from 'next/navigation'
interface LegacyBtcPageProps {
params: Promise<{ locale: string }>
@@ -6,5 +6,5 @@ interface LegacyBtcPageProps {
export default async function LegacyBtcPage({ params }: LegacyBtcPageProps) {
const { locale } = await params
- redirect(`/${locale}/macro`)
+ permanentRedirect(`/${locale}/macro`)
}
diff --git a/app/[locale]/case-studies/page.tsx b/app/[locale]/case-studies/page.tsx
index 3a79996..6414488 100644
--- a/app/[locale]/case-studies/page.tsx
+++ b/app/[locale]/case-studies/page.tsx
@@ -173,14 +173,14 @@ function getCopy(locale: string): CaseStudiesCopy {
ogDescription:
'Historical cases that show why the Trump, BTC, and KOL engines exist: posts, wallet behavior, price reaction, and evidence.',
heroTitle: 'Case Studies',
- heroSubtitle: 'Real events, real positioning, and real price reactions — documented.',
+ heroSubtitle: 'Real events, real price moves — why these signals are worth tracking.',
intro:
- 'This is not a performance-claims page. It is an evidence page. Each case answers three questions: what happened, how the market reacted, and why that event supports one of the signal engines.',
+ 'Three documented cases where the signal worked. Not forecasts — history.',
statAsset: 'Asset',
- statImpact: 'Price impact',
+ statImpact: 'Move',
statWindow: 'Window',
- evidenceLabel: 'Evidence',
- footer: 'These cases illustrate the historical basis for each signal engine. They are not return forecasts.',
+ evidenceLabel: 'Source',
+ footer: 'Past events don\'t guarantee future results. These cases show why the signal categories exist.',
footerMethodology: 'Methodology',
footerGlossary: 'Glossary',
cases: [
@@ -188,85 +188,51 @@ function getCopy(locale: string): CaseStudiesCopy {
id: 'strategic-reserve-2025',
date: 'March 2, 2025',
source: 'Trump Truth Social',
- title: 'Strategic Crypto Reserve announcement',
- summary: 'Trump confirmed a U.S. Strategic Crypto Reserve including BTC, ETH, SOL, XRP, and ADA.',
+ title: 'Trump announces U.S. Strategic Crypto Reserve — BTC +8%, ETH +10%',
+ summary: 'Trump posted confirmation of a U.S. Strategic Crypto Reserve including BTC, ETH, SOL, XRP, and ADA. Markets moved within minutes.',
signal: 'LONG',
asset: 'BTC / ETH',
priceImpact: 'BTC +8.2%, ETH +10%+',
impactWindow: '24 hours',
detail:
- 'This case mattered because the post moved from vague political tone into explicit asset-level policy language. A signal engine should classify that as a high-conviction LONG event, because it names specific assets and ties them to a state-level commitment.',
+ 'The post named specific assets and tied them to a government-level commitment — the clearest possible bullish signal. This is exactly what the Trump signal engine is built to catch: a post that goes from vague political tone to explicit crypto policy in one statement.',
evidence:
- 'CNBC and Al Jazeera both documented the market response, and exchange plus on-chain data showed clear momentum-driven inflows after the post.',
+ 'CNBC and Al Jazeera documented the market response. Exchange data showed immediate momentum-driven inflows.',
externalUrl:
'https://www.cnbc.com/2025/03/02/trump-announces-strategic-crypto-reserve-including-bitcoin-solana-xrp-and-more.html',
},
- {
- id: 'whale-6m-perp',
- date: 'March 2025',
- source: 'On-chain / Truth Social',
- title: '$6.8M pre-positioning profit around the reserve post',
- summary:
- 'An anonymous address built a large leveraged BTC/ETH position around the reserve announcement and reportedly closed it the same day for roughly $6.8M profit.',
- signal: 'LONG',
- asset: 'BTC',
- priceImpact: '+$6.8M',
- impactWindow: 'Same day',
- detail:
- 'This case highlights two facts at once. First, Trump-linked posts can create enough directional force to matter. Second, speed itself is edge in event-driven trading. Whether this was advance positioning or ultra-fast automation, it validates the post-to-trade-to-PnL chain.',
- evidence:
- 'The trade was documented by Raw Story / NewsBreak, and TIME later referenced the political and regulatory scrutiny around the episode.',
- externalUrl:
- 'https://www.newsbreak.com/raw-story-2096750/4286876592634-that-s-the-maga-playbook-crypto-trade-made-moments-before-trump-post-raises-eyebrows',
- },
{
id: 'tariff-china-2025',
date: 'April 2025',
source: 'Trump Truth Social',
- title: 'Tariff escalation post triggered a risk-off move',
- summary: 'A tariff-escalation post created a short-term macro risk-off setup rather than a bullish crypto reaction.',
+ title: 'Tariff escalation post — BTC -4% in 4 hours',
+ summary: 'A tariff-escalation post compressed risk appetite fast. BTC dropped 4.1% in 4 hours — no crypto-specific news, just macro sentiment repricing.',
signal: 'SHORT',
asset: 'BTC',
priceImpact: 'BTC -4.1%',
impactWindow: '4 hours',
detail:
- 'Not every Trump post is bullish for crypto. Posts framed around trade conflict, tariff escalation, tighter regulation, or macro uncertainty can compress risk appetite quickly. This case supports the need for a real SHORT branch in the signal engine.',
+ 'Not every Trump post is bullish for crypto. Trade conflict, tariffs, and macro uncertainty posts can hit crypto just as hard as equity markets. This is why the signal engine has a real SHORT branch — and why noise filtering matters.',
evidence:
- 'CoinDesk has documented multiple instances of Trump statements moving BTC. This post fits that broader historical pattern of immediate sentiment repricing.',
+ 'CoinDesk has documented multiple Trump statements that moved BTC in both directions.',
externalUrl:
'https://www.coindesk.com/markets/2026/04/20/five-times-president-trump-made-a-statement-that-moved-bitcoin-and-why-it-might-happen-again-this-week',
},
{
id: 'kol-divergence-example',
- date: 'Illustrative composite',
- source: 'KOL post + on-chain wallet',
- title: 'Publicly bullish on ETH, privately reducing ETH',
+ date: 'Composite example',
+ source: 'KOL post + tracked wallet',
+ title: 'KOL bullish on ETH in public, selling ETH on-chain',
summary:
- 'A tracked KOL published a bullish ETH thesis, then reduced roughly $180k of ETH exposure within five days. That is classic talks-vs-trades divergence.',
+ 'A tracked KOL published a bullish ETH thesis. Within 5 days, their wallet reduced ~$180k of ETH exposure. Public pitch said buy — the wallet said sell.',
signal: 'DIVERGENCE',
asset: 'ETH',
priceImpact: 'N/A',
impactWindow: '±7 days',
detail:
- 'This example is not about a single post moving price. It is about the information gap between public narrative and real positioning. For research-heavy users, that gap is often more valuable than a simple bullish or bearish statement.',
+ 'Words are free. Wallet moves cost money. When a KOL\'s on-chain behavior contradicts their public call, the wallet is usually telling the truth. This is why the KOL module tracks both — and why divergence is the highest-conviction category.',
evidence:
- 'The divergence classification comes from the platform’s cross-signal logic, which is documented in full on the Methodology page.',
- },
- {
- id: 'btc-bottom-nov-2022',
- date: 'November 2022',
- source: 'Historical BTC macro-bottom reference',
- title: 'The $15.5k FTX washout zone',
- summary:
- 'After the FTX collapse, BTC entered the $15.5k region and later recovered toward $69k within roughly 18 months. This is the kind of regime the BTC macro-bottom engine is built to identify.',
- signal: 'LONG',
- asset: 'BTC',
- priceImpact: '+345%',
- impactWindow: '18 months',
- detail:
- 'The current live BTC engine is not trying to replay an old premium-data model. It is trying to locate the same market structure with AHR999, the 200-week moving average, and Pi Cycle Bottom: deep value, long-cycle support, and emotional washout lining up together.',
- evidence:
- 'Public price archives and long-range market charts consistently mark the FTX washout zone as the core bottom region of that bear market.',
+ 'Divergence detection logic is documented on the Methodology page.',
},
],
}
diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css
index 9e8a3a0..c8ef483 100644
--- a/app/[locale]/globals.css
+++ b/app/[locale]/globals.css
@@ -44,9 +44,9 @@
--r-pill: 999px;
/* Shadow */
- --shadow-1: 0 1px 2px rgba(20, 18, 14, 0.04), 0 1px 1px rgba(20, 18, 14, 0.02);
- --shadow-2: 0 4px 16px rgba(20, 18, 14, 0.06), 0 1px 2px rgba(20, 18, 14, 0.03);
- --shadow-3: 0 12px 40px rgba(20, 18, 14, 0.08), 0 2px 4px rgba(20, 18, 14, 0.04);
+ --shadow-1: 0 1px 3px rgba(20, 18, 14, 0.07), 0 1px 2px rgba(20, 18, 14, 0.04);
+ --shadow-2: 0 4px 20px rgba(20, 18, 14, 0.09), 0 1px 4px rgba(20, 18, 14, 0.05);
+ --shadow-3: 0 16px 48px rgba(20, 18, 14, 0.12), 0 2px 6px rgba(20, 18, 14, 0.06);
--sans: 'Geist', ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
--mono: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace;
@@ -80,20 +80,15 @@ html[data-theme="dark"] {
--violet: oklch(70% 0.17 280);
--violet-soft: oklch(24% 0.06 280);
- --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.3);
- --shadow-2: 0 4px 16px rgba(0, 0, 0, 0.35);
- --shadow-3: 0 12px 40px rgba(0, 0, 0, 0.5);
+ --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.4);
+ --shadow-2: 0 4px 20px rgba(0, 0, 0, 0.45);
+ --shadow-3: 0 16px 48px rgba(0, 0, 0, 0.6);
color-scheme: dark;
}
html[data-theme="dark"] .src-ico.x { background: oklch(92% 0.005 85); color: oklch(15% 0.008 85); }
-html[data-theme="dark"] .bot-status {
- background: linear-gradient(160deg, oklch(28% 0.015 85) 0%, oklch(20% 0.01 85) 100%);
- border: 1px solid oklch(30% 0.01 85);
-}
-
html[data-theme="dark"] .kpi.accent {
background: linear-gradient(135deg, oklch(28% 0.05 75), oklch(24% 0.07 70));
}
@@ -103,8 +98,9 @@ html[data-theme="dark"] .kpi.accent {
on the dark grid as a glaring out-of-theme block. Mirror the kpi dark
gradient so the card stays visually grouped with everything else. */
html[data-theme="dark"] .overview-stat-card.accent {
- background: linear-gradient(135deg, oklch(28% 0.05 75), oklch(24% 0.07 70));
- border-color: color-mix(in oklab, var(--amber) 28%, var(--line));
+ background: var(--surface);
+ border-color: var(--line);
+ border-left: 3px solid var(--amber);
}
html[data-theme="dark"] .landing-nav {
@@ -112,7 +108,7 @@ html[data-theme="dark"] .landing-nav {
}
html[data-theme="dark"] .src-ico.truth {
- background: oklch(30% 0.08 25); color: oklch(80% 0.15 25);
+ background: oklch(28% 0.10 25); color: oklch(82% 0.18 25); font-weight: 700;
}
html[data-theme="dark"] .post-row.selected {
@@ -133,6 +129,13 @@ html, body {
* { box-sizing: border-box; }
+/* Global focus ring — amber for brand consistency, only on keyboard nav */
+:focus-visible {
+ outline: 2px solid var(--amber);
+ outline-offset: 2px;
+ border-radius: 4px;
+}
+
button { font-family: inherit; cursor: pointer; border: 0; background: transparent; color: inherit; }
input, textarea { font-family: inherit; }
a { color: inherit; text-decoration: none; }
@@ -152,16 +155,20 @@ a { color: inherit; text-decoration: none; }
/* ---------- Top navbar ---------- */
.nav {
- height: 64px;
+ height: 60px;
padding: 0 28px;
display: flex;
align-items: center;
- gap: 32px;
- background: var(--bg);
+ gap: 28px;
+ background: color-mix(in oklab, var(--bg) 88%, transparent);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 40;
+ box-shadow: 0 1px 0 color-mix(in oklab, var(--line) 50%, transparent),
+ 0 4px 16px rgba(0,0,0,0.04);
}
.wallet-anchor {
@@ -217,22 +224,23 @@ a { color: inherit; text-decoration: none; }
display: flex;
align-items: center;
gap: 10px;
- font-weight: 600;
+ font-weight: 700;
font-size: 15px;
- letter-spacing: -0.01em;
+ letter-spacing: -0.02em;
}
.brand-mark {
- width: 28px;
- height: 28px;
- border-radius: 8px;
- background: var(--ink);
- color: var(--bg);
+ width: 30px;
+ height: 30px;
+ border-radius: 9px;
+ background: linear-gradient(145deg, oklch(26% 0.015 75) 0%, oklch(13% 0.006 85) 100%);
+ color: oklch(96% 0.006 85);
display: grid;
place-items: center;
font-family: var(--mono);
font-size: 15px;
- font-weight: 500;
+ font-weight: 600;
position: relative;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.08);
}
.brand-mark::after {
content: '';
@@ -244,30 +252,33 @@ a { color: inherit; text-decoration: none; }
border-radius: 999px;
background: var(--amber);
border: 2px solid var(--bg);
+ box-shadow: 0 1px 4px color-mix(in oklab, var(--amber) 50%, transparent);
}
.nav-tabs {
display: flex;
align-items: center;
- gap: 4px;
+ gap: 3px;
padding: 4px;
background: var(--bg-sunk);
border: 1px solid var(--line);
border-radius: var(--r-pill);
}
.nav-tab {
- padding: 7px 14px;
+ padding: 7px 15px;
border-radius: var(--r-pill);
font-size: 13px;
font-weight: 500;
color: var(--ink-3);
transition: color 120ms, background 120ms;
}
-.nav-tab:hover { color: var(--ink); }
+.nav-tab:hover { color: var(--ink-2); }
.nav-tab.active {
- background: var(--surface);
+ background: color-mix(in oklab, var(--amber) 7%, var(--surface));
color: var(--ink);
- box-shadow: var(--shadow-1);
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.7);
+ border: 1px solid color-mix(in oklab, var(--amber) 22%, transparent);
+ font-weight: 600;
}
.nav-spacer { flex: 1; }
@@ -294,24 +305,41 @@ a { color: inherit; text-decoration: none; }
.connect-btn {
padding: 12px 22px;
border-radius: var(--r-pill);
- background: var(--ink);
- color: var(--bg);
+ background: linear-gradient(135deg, oklch(24% 0.012 80) 0%, oklch(14% 0.006 85) 100%);
+ color: oklch(97% 0.005 85);
font-size: 14px;
font-weight: 600;
letter-spacing: -0.005em;
- transition: background 120ms, transform 120ms, box-shadow 120ms;
- box-shadow: var(--shadow-1);
+ transition: opacity 120ms, transform 120ms, box-shadow 120ms;
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.07);
}
-.connect-btn:hover { background: oklch(30% 0.01 85); box-shadow: var(--shadow-2); }
+.connect-btn:hover { opacity: 0.88; box-shadow: var(--shadow-2); }
.connect-btn:active { transform: scale(0.98); }
.connect-btn.lg { padding: 13px 26px; font-size: 14px; }
-html[data-theme="dark"] .connect-btn { background: var(--amber); color: oklch(20% 0.04 75); }
-html[data-theme="dark"] .connect-btn:hover { background: oklch(82% 0.17 75); }
+html[data-theme="dark"] .connect-btn {
+ background: linear-gradient(135deg, var(--amber) 0%, oklch(73% 0.18 70) 100%);
+ color: oklch(18% 0.04 75);
+ box-shadow: 0 2px 10px color-mix(in oklab, var(--amber) 40%, transparent),
+ inset 0 1px 0 rgba(255,255,255,0.25);
+}
+html[data-theme="dark"] .connect-btn:hover { opacity: 0.9; }
.theme-toggle { width: 40px; height: 40px; }
html[data-theme="dark"] .icon-btn { background: var(--surface-2); border-color: var(--line); }
html[data-theme="dark"] .icon-btn:hover { background: var(--surface-3); }
-html[data-theme="dark"] .nav-tab.active { background: var(--surface-2); }
+html[data-theme="dark"] .card {
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.04);
+}
+html[data-theme="dark"] .overview-market-card,
+html[data-theme="dark"] .overview-side-card,
+html[data-theme="dark"] .overview-stats-bar {
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.03);
+}
+html[data-theme="dark"] .nav-tab.active {
+ background: color-mix(in oklab, var(--amber) 10%, var(--surface-2));
+ border-color: color-mix(in oklab, var(--amber) 28%, transparent);
+ box-shadow: var(--shadow-1);
+}
html[data-theme="dark"] .asset-switch button.on { background: var(--surface-2); }
.wallet-chip {
@@ -372,19 +400,21 @@ html[data-theme="dark"] .asset-switch button.on { background: var(--surface-2);
display: flex;
align-items: flex-end;
justify-content: space-between;
- margin-bottom: 24px;
+ margin-bottom: 28px;
gap: 24px;
}
.page-title {
- font-size: 28px;
- letter-spacing: -0.025em;
- font-weight: 700;
- line-height: 1.1;
+ font-size: 32px;
+ letter-spacing: -0.035em;
+ font-weight: 800;
+ line-height: 1.05;
margin: 0;
+ color: var(--ink);
}
.page-sub {
color: var(--ink-3);
- font-size: 13px;
+ font-size: 14px;
+ line-height: 1.5;
margin-top: 5px;
margin-bottom: 0;
}
@@ -394,18 +424,20 @@ html[data-theme="dark"] .asset-switch button.on { background: var(--surface-2);
gets read (page-sub was so faint it functioned as decoration).
Use via
One sentence about the page. . */
.page-hint {
- color: var(--ink-2);
- font-size: 16px;
+ color: var(--ink-3);
+ font-size: 14px;
line-height: 1.5;
- margin: 10px 0 0;
+ margin: 6px 0 0;
max-width: 64ch;
}
.page-hint-count {
- color: var(--ink-4);
- font-size: 13px;
+ color: var(--amber-ink);
+ font-size: 12px;
+ font-weight: 600;
margin-left: 10px;
padding-left: 10px;
border-left: 1px solid var(--line);
+ opacity: 0.8;
}
/* SectionHint — always-visible explanation block. Pairs with a section/tab
@@ -414,8 +446,9 @@ html[data-theme="dark"] .asset-switch button.on { background: var(--surface-2);
full content width on purpose — narrow boxes inside a wide page looked
like accidentally-floating callouts. */
.section-hint {
- background: var(--bg-sunk);
- border: 1px solid var(--line);
+ background: color-mix(in oklab, var(--amber) 4%, var(--bg-sunk));
+ border: 1px solid color-mix(in oklab, var(--amber) 18%, var(--line));
+ border-left: 3px solid color-mix(in oklab, var(--amber) 60%, transparent);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 14px;
@@ -515,6 +548,7 @@ html[data-theme="dark"] .infotip-bubble {
align-items: flex-start;
justify-content: space-between;
gap: 16px;
+ background: color-mix(in oklab, var(--amber) 3%, var(--surface));
}
.macro-panel-title {
@@ -523,6 +557,7 @@ html[data-theme="dark"] .infotip-bubble {
gap: 8px;
font-size: 15px;
font-weight: 700;
+ letter-spacing: -0.01em;
}
.macro-panel-dot {
@@ -531,6 +566,14 @@ html[data-theme="dark"] .infotip-bubble {
border-radius: 999px;
background: var(--up);
display: inline-block;
+ position: relative;
+ box-shadow: 0 0 5px color-mix(in oklab, var(--up) 60%, transparent);
+ animation: macro-dot-pulse 2.4s ease-out infinite;
+}
+@keyframes macro-dot-pulse {
+ 0%, 60% { box-shadow: 0 0 5px color-mix(in oklab, var(--up) 60%, transparent); }
+ 30% { box-shadow: 0 0 10px color-mix(in oklab, var(--up) 80%, transparent); }
+ 100% { box-shadow: 0 0 5px color-mix(in oklab, var(--up) 60%, transparent); }
}
.macro-panel-subtitle {
@@ -576,8 +619,10 @@ html[data-theme="dark"] .infotip-bubble {
width: 5px;
height: 5px;
border-radius: 50%;
- background: var(--ink-3);
+ background: var(--amber);
flex-shrink: 0;
+ box-shadow: 0 0 5px color-mix(in oklab, var(--amber) 50%, transparent);
+ opacity: 0.8;
}
.macro-grid {
@@ -607,7 +652,7 @@ html[data-theme="dark"] .infotip-bubble {
}
.macro-metric-card {
- box-shadow: var(--shadow-1);
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.6);
}
.macro-metric-card.tone-up {
@@ -737,30 +782,28 @@ html[data-theme="dark"] .infotip-bubble {
letter-spacing: 0.01em;
}
.macro-threshold-chip.up.active {
- background: var(--up);
+ background: linear-gradient(135deg, var(--up) 0%, oklch(66% 0.19 155) 100%);
color: #fff;
border-color: var(--up);
box-shadow:
- 0 6px 18px color-mix(in oklab, var(--up) 35%, transparent),
- 0 1px 0 rgba(255,255,255,0.25) inset;
+ 0 6px 18px color-mix(in oklab, var(--up) 40%, transparent),
+ inset 0 1px 0 rgba(255,255,255,0.28);
}
.macro-threshold-chip.down.active {
- background: var(--down);
+ background: linear-gradient(135deg, var(--down) 0%, oklch(55% 0.24 18) 100%);
color: #fff;
border-color: var(--down);
box-shadow:
- 0 6px 18px color-mix(in oklab, var(--down) 35%, transparent),
- 0 1px 0 rgba(255,255,255,0.25) inset;
+ 0 6px 18px color-mix(in oklab, var(--down) 40%, transparent),
+ inset 0 1px 0 rgba(255,255,255,0.22);
}
.macro-threshold-chip.neutral.active {
- /* Amber = "caution" signal — between safe and danger. Lets the user
- know "you are here, no extreme yet" without the alarming red. */
- background: var(--amber);
+ background: linear-gradient(135deg, var(--amber) 0%, oklch(70% 0.19 65) 100%);
color: oklch(20% 0.04 75);
border-color: var(--amber);
box-shadow:
- 0 6px 18px color-mix(in oklab, var(--amber) 35%, transparent),
- 0 1px 0 rgba(255,255,255,0.4) inset;
+ 0 6px 18px color-mix(in oklab, var(--amber) 40%, transparent),
+ inset 0 1px 0 rgba(255,255,255,0.45);
}
/* Dark mode tweaks — the soft white-shimmer inset doesn't read on dark cards,
@@ -823,26 +866,26 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
align-items: center;
gap: 6px;
/* Compact natural width — fits "Open chart CoinGlass ↗" comfortably */
- padding: 7px 12px;
- border-radius: 8px;
+ padding: 7px 13px;
+ border-radius: 9px;
border: 1px solid var(--line);
background: color-mix(in oklab, var(--ink) 4%, var(--surface));
color: var(--ink);
text-decoration: none;
transition: background 120ms, border-color 120ms, transform 120ms, box-shadow 120ms;
- box-shadow: inset 0 1px 0 rgba(255,255,255,0.4);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), var(--shadow-1);
}
.macro-action-btn:hover {
- background: color-mix(in oklab, var(--ink) 7%, var(--surface));
- border-color: color-mix(in oklab, var(--ink) 12%, var(--line));
+ background: color-mix(in oklab, var(--ink) 8%, var(--surface));
+ border-color: color-mix(in oklab, var(--ink) 14%, var(--line));
transform: translateY(-1px);
- box-shadow: var(--shadow-1);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), var(--shadow-2);
}
.macro-action-btn.chart {
- background: color-mix(in oklab, var(--amber) 10%, var(--surface));
- border-color: color-mix(in oklab, var(--amber) 18%, var(--line));
+ background: color-mix(in oklab, var(--amber) 12%, var(--surface));
+ border-color: color-mix(in oklab, var(--amber) 22%, var(--line));
}
/* The kicker ("OPEN CHART" uppercase eyebrow) used to sit ABOVE the label
@@ -911,6 +954,7 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
metric cards rather than a full-bleed banner. */
margin: 14px;
padding: 20px 22px 22px;
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.5);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 12px;
@@ -929,8 +973,9 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
- color: var(--ink-3);
+ color: var(--amber-ink);
font-weight: 700;
+ opacity: 0.85;
}
.macro-composite-right {
@@ -952,12 +997,13 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
display: inline-flex;
align-items: center;
min-height: 28px;
- padding: 0 10px;
+ padding: 0 12px;
border-radius: 999px;
border: 1px solid var(--line);
font-size: 11px;
font-weight: 700;
- letter-spacing: 0.06em;
+ letter-spacing: 0.07em;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.35);
}
.macro-composite-track {
@@ -1063,6 +1109,7 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 20px;
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.55);
}
.card.flush { padding: 0; }
.card.soft { background: var(--bg-sunk); }
@@ -1076,9 +1123,9 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
}
.section-title h2 {
margin: 0;
- font-size: 16px;
- font-weight: 600;
- letter-spacing: -0.01em;
+ font-size: 17px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
}
.section-title .hint {
font-size: 12px;
@@ -1096,21 +1143,27 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
font-size: 12px;
color: var(--ink-2);
font-weight: 500;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.45);
}
.chip .dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
-.chip.up { color: oklch(40% 0.16 148); background: var(--up-soft); border-color: oklch(85% 0.08 148); }
-.chip.down { color: oklch(44% 0.2 25); background: var(--down-soft); border-color: oklch(87% 0.08 25); }
-.chip.amber { color: var(--amber-ink); background: var(--amber-soft); border-color: var(--amber-ring); }
-.chip.violet { color: oklch(38% 0.15 280); background: var(--violet-soft); border-color: oklch(86% 0.08 280); }
+/* Text darkened to clear WCAG AA 4.5:1 on the soft tint (was 3.4:1). */
+.chip.up { color: oklch(33% 0.17 150); background: var(--up-soft); border-color: oklch(84% 0.10 148); font-weight: 600; }
+.chip.down { color: oklch(36% 0.20 27); background: var(--down-soft); border-color: oklch(86% 0.10 25); font-weight: 600; }
+/* Dark mode: soft tint is dark, so the text must go light to keep contrast. */
+html[data-theme="dark"] .chip.up { color: oklch(83% 0.15 150); }
+html[data-theme="dark"] .chip.down { color: oklch(80% 0.16 27); }
+.chip.amber { color: var(--amber-ink); background: var(--amber-soft); border-color: var(--amber-ring); font-weight: 600; }
+.chip.violet { color: oklch(36% 0.17 280); background: var(--violet-soft); border-color: oklch(84% 0.10 280); font-weight: 600; }
.chip.neutral { color: var(--ink-2); }
.live-dot {
- width: 6px;
- height: 6px;
+ width: 7px;
+ height: 7px;
border-radius: 999px;
background: var(--up);
position: relative;
display: inline-block;
+ box-shadow: 0 0 6px color-mix(in oklab, var(--up) 60%, transparent);
}
.live-dot::after {
content: '';
@@ -1118,12 +1171,12 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
inset: -4px;
border-radius: 999px;
background: var(--up);
- opacity: 0.3;
- animation: pulse 1.6s ease-out infinite;
+ opacity: 0.35;
+ animation: pulse 1.8s ease-out infinite;
}
@keyframes pulse {
- 0% { transform: scale(0.6); opacity: 0.5; }
- 100% { transform: scale(2.2); opacity: 0; }
+ 0% { transform: scale(0.5); opacity: 0.6; }
+ 100% { transform: scale(2.6); opacity: 0; }
}
.btn {
@@ -1138,22 +1191,32 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
transition: background 120ms, transform 120ms;
border: 1px solid transparent;
}
-.btn.primary { background: var(--ink); color: var(--bg); }
-.btn.primary:hover { background: oklch(30% 0.01 85); }
-.btn.amber {
- background: var(--amber);
- color: oklch(22% 0.04 75);
- font-weight: 600;
+.btn.primary {
+ background: linear-gradient(135deg, oklch(24% 0.012 80) 0%, oklch(14% 0.006 85) 100%);
+ color: oklch(97% 0.005 85);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.07);
+}
+.btn.primary:hover { opacity: 0.88; }
+.btn.amber {
+ background: linear-gradient(135deg, var(--amber) 0%, oklch(73% 0.18 70) 100%);
+ color: oklch(20% 0.04 75);
+ font-weight: 700;
+ box-shadow: 0 2px 8px color-mix(in oklab, var(--amber) 40%, transparent),
+ inset 0 1px 0 rgba(255,255,255,0.35);
+}
+.btn.amber:hover {
+ background: linear-gradient(135deg, oklch(82% 0.17 75) 0%, oklch(76% 0.19 68) 100%);
+ box-shadow: 0 4px 14px color-mix(in oklab, var(--amber) 45%, transparent),
+ inset 0 1px 0 rgba(255,255,255,0.35);
}
-.btn.amber:hover { background: oklch(82% 0.17 75); }
.btn.ghost { background: var(--bg-sunk); color: var(--ink); border-color: var(--line); }
.btn.ghost:hover { background: var(--surface-3); }
.btn.lg { padding: 14px 22px; font-size: 15px; }
.btn:active { transform: scale(0.98); }
-.delta { font-family: var(--mono); font-weight: 500; }
-.delta.up { color: oklch(42% 0.16 148); }
-.delta.down { color: oklch(46% 0.2 25); }
+.delta { font-family: var(--mono); font-weight: 600; }
+.delta.up { color: oklch(40% 0.18 148); }
+.delta.down { color: oklch(44% 0.22 25); }
/* ── iOS-style toggle switch ──────────────────────────────── */
.switch {
@@ -1188,9 +1251,15 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
transition: transform 160ms;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
}
-.switch input:checked + .switch-track { background: var(--amber); }
+.switch input:checked + .switch-track {
+ background: linear-gradient(135deg, var(--amber) 0%, oklch(73% 0.18 70) 100%);
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--amber) 18%, transparent);
+}
.switch input:checked + .switch-track::before { transform: translateX(14px); }
-.switch.up input:checked + .switch-track { background: var(--up); }
+.switch.up input:checked + .switch-track {
+ background: linear-gradient(135deg, var(--up) 0%, oklch(66% 0.19 155) 100%);
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--up) 18%, transparent);
+}
.switch.down input:checked + .switch-track { background: var(--down); }
/* ── Form row: label | control, strict-aligned ─────────────── */
@@ -1236,17 +1305,17 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
}
.section-head:first-child { border-top: 0; padding-top: 0; }
.section-head-label {
- font-size: 11px;
- font-weight: 600;
- letter-spacing: 0.08em;
+ font-size: 10.5px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
text-transform: uppercase;
- color: var(--ink-4);
-}
-
-.settings-scope-intro {
- margin-bottom: 16px;
+ color: var(--amber-ink);
+ opacity: 0.75;
}
+/* ── Settings legacy classes (removed from HTML 2026-06) ──────────────── */
+/* These are kept as empty stubs so any cached HTML from CDN doesn't break. */
+.settings-scope-intro,
.settings-control-center {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(320px, 1fr);
@@ -1496,8 +1565,8 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
.hero-value {
font-size: 44px;
- font-weight: 500;
- letter-spacing: -0.025em;
+ font-weight: 600;
+ letter-spacing: -0.03em;
line-height: 1;
font-family: var(--mono);
font-feature-settings: 'tnum';
@@ -1523,9 +1592,10 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
gap: 6px;
}
.asset-switch button.on {
- background: var(--surface);
+ background: color-mix(in oklab, var(--amber) 6%, var(--surface));
color: var(--ink);
- box-shadow: var(--shadow-1);
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.6);
+ border: 1px solid color-mix(in oklab, var(--amber) 20%, transparent);
}
.asset-dot {
width: 14px;
@@ -1551,7 +1621,11 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
color: var(--ink-3);
font-family: var(--mono);
}
-.tf-bar button.on { background: var(--ink); color: var(--bg); }
+.tf-bar button.on {
+ background: linear-gradient(135deg, oklch(24% 0.012 80) 0%, oklch(14% 0.006 85) 100%);
+ color: oklch(97% 0.005 85);
+ box-shadow: 0 1px 4px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.07);
+}
.chart-wrap {
margin-top: 8px;
@@ -1562,14 +1636,15 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
.chart-footnote {
display: flex;
gap: 18px;
- font-size: 12px;
+ font-size: 11.5px;
color: var(--ink-3);
border-top: 1px dashed var(--line);
padding-top: 14px;
margin-top: 4px;
+ letter-spacing: 0.01em;
}
.chart-footnote .item { display: flex; align-items: center; gap: 6px; }
-.legend-dot { width: 8px; height: 8px; border-radius: 2px; }
+.legend-dot { width: 8px; height: 8px; border-radius: 3px; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }
/* KPI tiles */
.kpi-row {
@@ -1588,18 +1663,22 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
gap: 6px;
position: relative;
overflow: hidden;
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.5);
}
.kpi .label {
- font-size: 12px;
+ font-size: 11px;
color: var(--ink-3);
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
display: flex;
align-items: center;
gap: 6px;
}
.kpi .value {
- font-size: 22px;
- font-weight: 500;
- letter-spacing: -0.01em;
+ font-size: 24px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
font-family: var(--mono);
}
.kpi .foot {
@@ -1610,16 +1689,17 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
gap: 6px;
}
.kpi.accent {
- background: linear-gradient(135deg, oklch(97% 0.04 85), oklch(95% 0.07 80));
+ background: linear-gradient(135deg, oklch(97% 0.05 82), oklch(95% 0.09 76));
border-color: var(--amber-ring);
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.7);
}
.kpi.accent .label { color: var(--amber-ink); }
/* Overview */
.overview-shell {
display: grid;
- grid-template-columns: minmax(0, 1fr) 320px;
- gap: 18px;
+ grid-template-columns: minmax(0, 1fr) 300px;
+ gap: 14px;
margin-bottom: 20px;
align-items: start;
}
@@ -1627,7 +1707,7 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
.overview-main {
display: flex;
flex-direction: column;
- gap: 16px;
+ gap: 12px;
min-width: 0;
}
@@ -1642,24 +1722,29 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
.overview-stat-card {
background: var(--surface);
border: 1px solid var(--line);
- border-radius: 14px;
- padding: 18px;
- box-shadow: var(--shadow-1);
+ border-radius: 16px;
+ padding: 20px;
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.55);
min-width: 0;
}
.overview-market-card {
background:
- linear-gradient(135deg, color-mix(in oklab, var(--amber) 8%, var(--surface)) 0%, var(--surface) 52%, color-mix(in oklab, var(--up) 6%, var(--surface)) 100%);
+ linear-gradient(135deg,
+ color-mix(in oklab, var(--amber) 12%, var(--surface)) 0%,
+ var(--surface) 48%,
+ color-mix(in oklab, var(--up) 8%, var(--surface)) 100%);
+ box-shadow: var(--shadow-2), inset 0 1px 0 rgba(255,255,255,0.65);
}
.overview-kicker {
font-size: 10px;
font-weight: 700;
- letter-spacing: 0.1em;
+ letter-spacing: 0.12em;
text-transform: uppercase;
- color: var(--ink-4);
+ color: var(--amber-ink);
margin-bottom: 10px;
+ opacity: 0.85;
}
.overview-card-head {
@@ -1785,8 +1870,8 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
.overview-score-pill {
display: inline-flex;
align-items: center;
- height: 22px;
- padding: 0 10px;
+ height: 24px;
+ padding: 0 11px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--surface);
@@ -1795,37 +1880,43 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-3);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.5);
}
.overview-score-pill.bull {
- color: var(--up);
+ /* Darkened from var(--up) (62% → 3.0:1) to clear AA 4.5:1 on the 12% tint. */
+ color: oklch(40% 0.17 150);
border-color: color-mix(in oklab, var(--up) 32%, var(--line));
background: color-mix(in oklab, var(--up) 12%, var(--surface));
}
.overview-score-pill.bear {
- color: var(--down);
+ color: oklch(42% 0.20 27);
border-color: color-mix(in oklab, var(--down) 32%, var(--line));
background: color-mix(in oklab, var(--down) 12%, var(--surface));
}
+/* Dark mode: tint background is dark, so revert to the bright tone tokens. */
+html[data-theme="dark"] .overview-score-pill.bull { color: var(--up); }
+html[data-theme="dark"] .overview-score-pill.bear { color: var(--down); }
+
/* Gradient track. Removed the .overview-score-fill overlay — the gradient
already encodes "where am I on the spectrum"; layering an opaque fill
only obscured the gradient at scores near 0. */
.overview-score-track {
position: relative;
- height: 14px;
+ height: 16px;
margin-top: 14px;
border-radius: 999px;
overflow: visible;
border: 1px solid color-mix(in oklab, var(--line) 78%, transparent);
- box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.06);
+ box-shadow: inset 0 1px 3px rgba(15, 23, 42, 0.1), inset 0 -1px 0 rgba(255,255,255,0.3);
background: linear-gradient(90deg,
- color-mix(in oklab, var(--down) 65%, transparent) 0%,
- color-mix(in oklab, var(--down) 18%, transparent) 35%,
- var(--bg-sunk) 50%,
- color-mix(in oklab, var(--up) 18%, transparent) 65%,
- color-mix(in oklab, var(--up) 65%, transparent) 100%);
+ color-mix(in oklab, var(--down) 70%, transparent) 0%,
+ color-mix(in oklab, var(--down) 20%, transparent) 35%,
+ color-mix(in oklab, var(--bg-sunk) 90%, transparent) 50%,
+ color-mix(in oklab, var(--up) 20%, transparent) 65%,
+ color-mix(in oklab, var(--up) 70%, transparent) 100%);
}
/* Vertical centerline tick so 0 is visually obvious. */
@@ -1873,71 +1964,127 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
}
.overview-system-strip {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 10px;
- margin-top: 16px;
+ display: none; /* replaced by .overview-stats-bar */
}
.overview-system-chip {
display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- min-height: 64px;
- padding: 12px 14px;
- border-radius: 12px;
- border: 1px solid var(--line);
- background: var(--surface);
- box-shadow: var(--shadow-1);
- transition: transform 120ms, border-color 120ms, box-shadow 120ms;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 2px;
+ flex: 1;
+ padding: 10px 16px;
+ border-right: 1px solid var(--line);
+ border-radius: 0;
+ border-top: none;
+ border-bottom: none;
+ border-left: none;
+ background: transparent;
+ box-shadow: none;
+ transition: background 100ms;
+}
+
+.overview-system-chip:last-child {
+ border-right: none;
}
.overview-system-chip:hover {
- transform: translateY(-1px);
- border-color: var(--line-2);
- box-shadow: var(--shadow-2);
+ transform: none;
+ box-shadow: none;
+ background: color-mix(in oklab, var(--amber) 5%, transparent);
}
.overview-system-chip.passive {
cursor: default;
}
+.overview-system-chip.passive:hover {
+ background: transparent;
+}
.overview-system-chip-name {
- font-size: 12px;
- line-height: 1.4;
- color: var(--ink-3);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--ink-4);
}
.overview-system-chip strong {
font-family: var(--mono);
- font-size: 24px;
- line-height: 1;
+ font-size: 20px;
+ line-height: 1.1;
+ font-weight: 700;
color: var(--ink);
}
-.overview-secondary-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- flex-wrap: wrap;
- gap: 14px;
+/* ── Unified stats bar (replaces system strip + secondary grid) ─── */
+.overview-stats-bar {
+ display: flex;
+ align-items: stretch;
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ overflow: hidden;
+ background: var(--surface);
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.55);
}
-.overview-stat-card {
- min-height: 144px;
+.stats-bar-item {
display: flex;
flex-direction: column;
- justify-content: space-between;
+ justify-content: center;
+ gap: 3px;
+ flex: 1;
+ padding: 12px 16px;
+ border-right: 1px solid var(--line);
+ background: transparent;
+ border-top: none;
+ border-bottom: none;
+ border-left: none;
+ transition: background 100ms;
+ text-decoration: none;
+ color: inherit;
}
-.overview-stat-card.accent {
- background: linear-gradient(135deg, oklch(97% 0.04 85), oklch(95% 0.07 80));
- border-color: var(--amber-ring);
+.stats-bar-item:not(.passive):hover {
+ background: color-mix(in oklab, var(--amber) 5%, transparent);
}
+.stats-bar-item.passive {
+ cursor: default;
+}
+
+/* Thin vertical separator between signal counts and personal stats */
+.stats-bar-divider {
+ width: 1px;
+ background: var(--line);
+ flex-shrink: 0;
+ align-self: stretch;
+ margin: 10px 0;
+}
+
+.stats-bar-label {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--ink-4);
+ white-space: nowrap;
+}
+
+.stats-bar-value {
+ font-family: var(--mono);
+ font-size: 22px;
+ font-weight: 800;
+ line-height: 1.1;
+ letter-spacing: -0.025em;
+}
+
+/* Keep old classes so nothing else breaks */
+.overview-secondary-grid { display: none; }
+.overview-stat-card { display: none; }
.overview-stat-value {
font-family: var(--mono);
- font-size: 42px;
+ font-size: 32px;
line-height: 1;
font-weight: 800;
letter-spacing: -0.03em;
@@ -1979,58 +2126,6 @@ html[data-theme="dark"] .macro-threshold-chip.neutral.active {
/* Right rail */
.rail { display: flex; flex-direction: column; gap: 20px; }
-.signal-card {
- background: var(--surface);
- border: 1px solid var(--line);
- border-radius: var(--r-md);
- padding: 18px;
-}
-.signal-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 14px;
-}
-.signal-head h3 {
- margin: 0;
- font-size: 14px;
- font-weight: 600;
-}
-
-.latest-post {
- border: 1px solid var(--line);
- border-radius: var(--r-sm);
- padding: 14px;
- background: var(--bg-sunk);
-}
-.latest-post .meta {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 12px;
- color: var(--ink-3);
- margin-bottom: 10px;
-}
-.latest-post .text {
- font-size: 13px;
- line-height: 1.55;
- color: var(--ink);
- margin: 0 0 12px;
-}
-.latest-post .divider {
- height: 1px;
- background: var(--line);
- margin: 12px 0;
-}
-.latest-post .lp-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 12px;
-}
-.latest-post .lp-row + .lp-row { margin-top: 6px; }
-.latest-post .lp-row .k { color: var(--ink-3); }
-
.confidence-bar {
height: 6px;
background: var(--surface-3);
@@ -2110,69 +2205,11 @@ html[data-theme="dark"] .ai-reasoning-card {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), var(--shadow-1);
}
-/* Bot status */
-.bot-status {
- background: linear-gradient(160deg, oklch(22% 0.01 85) 0%, oklch(14% 0.01 85) 100%);
- color: oklch(96% 0.005 85);
- border-radius: var(--r-lg);
- padding: 22px;
- position: relative;
- overflow: hidden;
-}
-.bot-status::after {
- content: '';
- position: absolute;
- right: -40px;
- top: -40px;
- width: 180px;
- height: 180px;
- background: radial-gradient(circle, oklch(75% 0.17 75 / 0.4) 0%, transparent 70%);
- pointer-events: none;
-}
-.bot-head {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 18px;
-}
-.bot-head h3 { margin: 0; font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
-.bot-stats {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 14px 20px;
- position: relative;
- z-index: 1;
-}
-.bot-stat .k {
- font-size: 11px;
- color: oklch(70% 0.01 85);
- text-transform: uppercase;
- letter-spacing: 0.08em;
- margin-bottom: 4px;
-}
-.bot-stat .v {
- font-size: 20px;
- font-family: var(--mono);
- font-weight: 500;
- letter-spacing: -0.01em;
-}
-.bot-stat .v.up { color: oklch(75% 0.17 148); }
-.bot-stat .v.amber { color: var(--amber); }
-
-.bot-cta {
- margin-top: 18px;
- display: flex;
- gap: 8px;
- position: relative;
- z-index: 1;
-}
-.bot-cta .btn { flex: 1; }
-
/* Post list (stream) */
.post-stream {
display: flex;
flex-direction: column;
- gap: 10px;
+ gap: 8px;
}
.post-row {
display: flex;
@@ -2180,12 +2217,37 @@ html[data-theme="dark"] .ai-reasoning-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
- transition: border-color 120ms, background 120ms;
+ transition: border-color 150ms, background 150ms, box-shadow 150ms;
cursor: pointer;
overflow: hidden;
+ box-shadow: var(--shadow-1);
+}
+.post-row:hover {
+ border-color: color-mix(in oklab, var(--amber) 25%, var(--line));
+ box-shadow: var(--shadow-2);
+ transform: translateY(-1px);
+}
+.post-row.selected {
+ border-color: color-mix(in oklab, var(--amber) 55%, var(--line));
+ background: color-mix(in oklab, var(--amber) 4%, var(--surface));
+ box-shadow: var(--shadow-2), 0 0 0 1px color-mix(in oklab, var(--amber) 20%, transparent);
+}
+/* Buy signal: green left accent */
+.post-row.signal-buy {
+ border-left: 4px solid var(--up);
+ background: linear-gradient(90deg, color-mix(in oklab, var(--up) 5%, var(--surface)) 0%, var(--surface) 160px);
+}
+/* Short signal: red left accent */
+.post-row.signal-short {
+ border-left: 4px solid var(--down);
+ background: linear-gradient(90deg, color-mix(in oklab, var(--down) 5%, var(--surface)) 0%, var(--surface) 160px);
+}
+html[data-theme="dark"] .post-row.signal-buy {
+ background: linear-gradient(90deg, color-mix(in oklab, var(--up) 10%, var(--surface)) 0%, var(--surface) 160px);
+}
+html[data-theme="dark"] .post-row.signal-short {
+ background: linear-gradient(90deg, color-mix(in oklab, var(--down) 10%, var(--surface)) 0%, var(--surface) 160px);
}
-.post-row:hover { border-color: var(--line-2); }
-.post-row.selected { border-color: var(--amber-ring); background: oklch(99% 0.02 85); }
/* Off-topic / noise posts: compact, recessed, low-emphasis. They're the bulk
of the Trump feed but carry no signal — make them read as skippable filler
@@ -2193,6 +2255,10 @@ html[data-theme="dark"] .ai-reasoning-card {
.post-row.noise {
background: var(--bg-sunk);
border-color: transparent;
+ opacity: 0.82;
+}
+.post-row.noise:hover {
+ opacity: 1;
}
.post-row.noise:hover { border-color: var(--line); }
.post-row.noise .post-row-main {
@@ -2235,7 +2301,7 @@ html[data-theme="dark"] .ai-reasoning-card {
flex-shrink: 0;
}
.src-ico.x { background: #111; color: #fff; }
-.src-ico.truth { background: oklch(94% 0.05 25); color: oklch(45% 0.2 25); }
+.src-ico.truth { background: oklch(92% 0.08 28); color: oklch(40% 0.22 25); font-weight: 700; }
/* Non-Trump sources — distinct hues so the feed reads at a glance. */
.src-ico.breakout { background: oklch(94% 0.08 200); color: oklch(40% 0.15 220); }
.src-ico.reversal { background: oklch(94% 0.08 300); color: oklch(40% 0.15 300); }
@@ -2269,15 +2335,30 @@ html[data-theme="dark"] .ai-reasoning-card {
display: inline-flex;
align-items: center;
gap: 6px;
- padding: 5px 10px;
+ padding: 4px 10px;
border-radius: var(--r-pill);
- font-size: 12px;
- font-weight: 600;
- letter-spacing: 0.02em;
+ font-size: 11px;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+}
+.sig.buy {
+ background: linear-gradient(135deg, var(--up) 0%, oklch(66% 0.19 155) 100%);
+ color: #fff;
+ box-shadow: 0 2px 8px color-mix(in oklab, var(--up) 45%, transparent),
+ inset 0 1px 0 rgba(255,255,255,0.2);
+}
+.sig.sell, .sig.short {
+ background: linear-gradient(135deg, var(--down) 0%, oklch(55% 0.24 18) 100%);
+ color: #fff;
+ box-shadow: 0 2px 8px color-mix(in oklab, var(--down) 45%, transparent),
+ inset 0 1px 0 rgba(255,255,255,0.15);
+}
+.sig.hold {
+ background: var(--bg-sunk);
+ color: var(--ink-3);
+ border: 1px solid var(--line);
+ font-weight: 600;
}
-.sig.buy { background: var(--up-soft); color: oklch(38% 0.16 148); }
-.sig.sell, .sig.short { background: var(--down-soft); color: oklch(42% 0.2 25); }
-.sig.hold { background: var(--bg-sunk); color: var(--ink-2); }
.impact-mini {
display: flex;
@@ -2297,11 +2378,13 @@ html[data-theme="dark"] .ai-reasoning-card {
}
.table th {
text-align: left;
- font-weight: 500;
+ font-weight: 700;
color: var(--ink-3);
- padding: 14px 18px;
+ padding: 12px 18px;
border-bottom: 1px solid var(--line);
- font-size: 12px;
+ font-size: 11px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
background: var(--bg-sunk);
}
.table th:first-child { border-top-left-radius: var(--r-md); }
@@ -2406,8 +2489,9 @@ html[data-theme="dark"] .ai-reasoning-card {
transition: transform 180ms;
}
.settings-switch.on {
- background: var(--ink);
- border-color: var(--ink);
+ background: linear-gradient(135deg, var(--amber) 0%, oklch(73% 0.18 70) 100%);
+ border-color: var(--amber);
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--amber) 20%, transparent);
}
.settings-switch.on::after { transform: translateX(16px); }
@@ -2438,6 +2522,23 @@ html[data-theme="dark"] .ai-reasoning-card {
order: -1;
}
+ /* Stats bar: 2 columns on tablet/narrow desktop */
+ .overview-stats-bar {
+ flex-wrap: wrap;
+ }
+ .stats-bar-item {
+ flex: 0 0 calc(50% - 1px);
+ border-right: 1px solid var(--line);
+ border-bottom: 1px solid var(--line);
+ }
+ .stats-bar-item:nth-child(2n) {
+ border-right: none;
+ }
+ .stats-bar-item:nth-last-child(-n+2) {
+ border-bottom: none;
+ }
+ .stats-bar-divider { display: none; }
+
.dash-grid {
grid-template-columns: 1fr;
}
@@ -2490,7 +2591,6 @@ html[data-theme="dark"] .ai-reasoning-card {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
- .overview-system-strip,
.overview-secondary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -2547,6 +2647,11 @@ html[data-theme="dark"] .ai-reasoning-card {
padding: 10px 14px;
}
+ .nav-tabs {
+ margin: 0 -14px;
+ padding: 0 14px;
+ }
+
.brand span:last-child {
display: none;
}
@@ -2585,11 +2690,17 @@ html[data-theme="dark"] .ai-reasoning-card {
text-align: left;
}
- .overview-system-strip,
.overview-secondary-grid {
grid-template-columns: 1fr;
}
+ .overview-system-strip {
+ flex-wrap: wrap;
+ }
+
+ /* Guest onboarding strip: 3-col → 1-col on mobile */
+ .guest-strip { grid-template-columns: 1fr !important; }
+
.macro-panel-head,
.macro-composite-head {
flex-direction: column;
@@ -2801,13 +2912,13 @@ body { max-width: 100%; }
.grow { flex: 1; }
.mono-num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--ink-3); }
-.tiny { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); font-weight: 500; }
+.tiny { font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--ink-3); font-weight: 600; }
/* Scrollbar */
-::-webkit-scrollbar { width: 10px; height: 10px; }
+::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 999px; border: 2px solid var(--bg); }
-::-webkit-scrollbar-thumb:hover { background: oklch(80% 0.01 85); }
+::-webkit-scrollbar-thumb:hover { background: color-mix(in oklab, var(--amber) 25%, var(--line-2)); }
/* ── Skeleton / shimmer loading ─────────────────────────────────────────────
Use .skeleton on any block element to get a shimmer placeholder.
@@ -2829,10 +2940,11 @@ body { max-width: 100%; }
animation: shimmer 1.4s ease-in-out infinite;
}
.skeleton-card {
- border-radius: var(--r);
+ border-radius: var(--r-md);
border: 1px solid var(--line);
padding: 20px;
- background: var(--bg-card);
+ background: var(--surface);
+ box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,0.5);
display: flex;
flex-direction: column;
gap: 10px;
@@ -2845,3 +2957,223 @@ body { max-width: 100%; }
.sk-w-3q { width: 75%; }
.sk-w-half { width: 50%; }
.sk-w-q { width: 25%; }
+
+/* ============================================================
+ Live "terminal" motion layer (A-tier + ticker)
+ All effects degrade to static under prefers-reduced-motion
+ (see the media query at the very end of this block).
+ ============================================================ */
+
+/* ── Tick flash: a value that changed pulses green/red briefly ─────────────── */
+@keyframes tick-flash-up {
+ 0% { color: var(--up); text-shadow: 0 0 10px color-mix(in oklab, var(--up) 55%, transparent); }
+ 100% { color: inherit; text-shadow: none; }
+}
+@keyframes tick-flash-down {
+ 0% { color: var(--down); text-shadow: 0 0 10px color-mix(in oklab, var(--down) 55%, transparent); }
+ 100% { color: inherit; text-shadow: none; }
+}
+.tick-num { transition: color 120ms; }
+.tick-flash-up { animation: tick-flash-up 0.6s ease-out 1; }
+.tick-flash-down { animation: tick-flash-down 0.6s ease-out 1; }
+
+/* ── New signal row: slide in from the top + an amber sweep across it ──────── */
+@keyframes signal-slide-in {
+ 0% { opacity: 0; transform: translateY(-10px); }
+ 100% { opacity: 1; transform: translateY(0); }
+}
+@keyframes signal-sweep {
+ 0% { background-position: -150% 0; }
+ 100% { background-position: 250% 0; }
+}
+.signal-enter { animation: signal-slide-in 0.32s cubic-bezier(.2,.8,.2,1) 1; }
+.signal-enter > * { position: relative; }
+.signal-enter > *::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: var(--r-md);
+ pointer-events: none;
+ background: linear-gradient(
+ 100deg,
+ transparent 0%,
+ color-mix(in oklab, var(--amber) 26%, transparent) 50%,
+ transparent 100%
+ );
+ background-size: 60% 100%;
+ background-repeat: no-repeat;
+ animation: signal-sweep 1.1s ease-out 1;
+}
+
+/* ── Card / chip hover glow — subtle amber ring lift on interactive cards ──── */
+.overview-system-chip,
+.overview-stat-card {
+ transition: border-color 160ms, box-shadow 220ms, transform 160ms;
+}
+.overview-system-chip:hover,
+.overview-stat-card:hover {
+ box-shadow: 0 0 0 1px var(--amber-ring),
+ 0 0 18px color-mix(in oklab, var(--amber) 22%, transparent);
+ transform: translateY(-1px);
+}
+
+/* ── Macro needle afterglow — a soft trailing pulse when it slides ─────────── */
+.overview-score-needle {
+ box-shadow: 0 0 0 0 color-mix(in oklab, currentColor 45%, transparent);
+ transition: left 0.4s cubic-bezier(.2,.8,.2,1), box-shadow 0.6s ease-out;
+}
+.overview-score-needle.bull,
+.overview-score-needle.bear { box-shadow: 0 0 14px 2px color-mix(in oklab, currentColor 40%, transparent); }
+
+/* ============================================================
+ Ticker tape (B-#6)
+ ============================================================ */
+@keyframes ticker-marquee {
+ 0% { transform: translateX(0); }
+ 100% { transform: translateX(-50%); }
+}
+.ticker {
+ position: sticky;
+ top: 0;
+ z-index: 90;
+ overflow: hidden;
+ border-bottom: 1px solid var(--line);
+ background: color-mix(in oklab, var(--amber) 2%, var(--bg-sunk));
+ /* fade the two edges so cells appear/vanish instead of hard-cutting */
+ -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
+ mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
+}
+.ticker-track {
+ display: inline-flex;
+ align-items: center;
+ white-space: nowrap;
+ will-change: transform;
+ animation: ticker-marquee 40s linear infinite;
+}
+.ticker:hover .ticker-track { animation-play-state: paused; }
+.ticker-cell {
+ display: inline-flex;
+ align-items: baseline;
+ gap: 7px;
+ padding: 7px 22px;
+ font-size: 12px;
+ font-family: var(--mono);
+ font-variant-numeric: tabular-nums;
+ border-right: 1px solid var(--line);
+}
+.ticker-sym { color: var(--amber-ink); font-weight: 700; letter-spacing: 0.05em; opacity: 0.8; }
+.ticker-px { color: var(--ink); font-weight: 500; transition: color 120ms; }
+.ticker-arrow { font-size: 9px; width: 9px; display: inline-block; }
+.ticker-arrow.up { color: var(--up); }
+.ticker-arrow.down { color: var(--down); }
+/* reuse the tick-flash keyframes on a whole ticker cell */
+.ticker-cell.tick-flash-up .ticker-px { animation: tick-flash-up 0.6s ease-out 1; }
+.ticker-cell.tick-flash-down .ticker-px { animation: tick-flash-down 0.6s ease-out 1; }
+html[data-theme="dark"] .ticker {
+ background: color-mix(in oklab, var(--amber) 4%, var(--bg-sunk));
+}
+
+/* ============================================================
+ Mobile wallet sheet
+ ============================================================ */
+@keyframes sheet-slide-up {
+ from { transform: translateY(100%); }
+ to { transform: translateY(0); }
+}
+.mobile-sheet-enter {
+ animation: sheet-slide-up 0.28s cubic-bezier(0.2, 0.8, 0.2, 1) both;
+}
+
+/* ── Accessibility: honour reduced-motion. Kill all the looping/entrance
+ motion above; keep static, readable end-states. ───────────────────────── */
+@media (prefers-reduced-motion: reduce) {
+ /* Catch-all: neutralise every animation/transition (incl. infinite ones —
+ macro-dot-pulse, live-dot pulse, shimmer, needle pulse — and any added
+ later). 0.01ms instead of 0 so JS onAnimationEnd handlers still fire. */
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+ /* Layout side-effects the duration reset alone can't undo: */
+ .ticker-track { transform: none; white-space: normal; flex-wrap: wrap; }
+ .signal-enter > *::after { display: none; }
+ .overview-system-chip:hover,
+ .overview-stat-card:hover { transform: none; }
+}
+
+/* ============================================================
+ Ticker tape — mobile overrides
+ ============================================================ */
+@media (max-width: 900px) {
+ .ticker {
+ /* On tablet/mobile the mask edges would eat too much of the narrow strip.
+ Tighten to 4% so content is visible closer to the edges. */
+ -webkit-mask-image: linear-gradient(90deg, transparent, #000 4%, #000 96%, transparent);
+ mask-image: linear-gradient(90deg, transparent, #000 4%, #000 96%, transparent);
+ }
+ .ticker-cell {
+ padding: 6px 14px;
+ gap: 5px;
+ }
+ .ticker-sym { font-size: 11px; }
+ .ticker-px { font-size: 11px; }
+}
+
+@media (max-width: 640px) {
+ /* Slow the tape a touch on mobile so it's readable while scrolling */
+ .ticker-track { animation-duration: 55s; }
+
+ .ticker-cell {
+ padding: 5px 12px;
+ font-size: 11px;
+ }
+
+ /* On very small screens, hide the direction arrow to save space */
+ .ticker-arrow { display: none; }
+
+ /* Overview hero price — keep it from bumping into the chip on mobile */
+ .overview-headline-row {
+ flex-wrap: wrap;
+ gap: 6px;
+ }
+
+ /* Prevent the overview market card controls from stacking awkwardly */
+ .overview-controls {
+ width: 100%;
+ align-items: flex-start;
+ }
+
+ /* Chart: reduce height on phones so it doesn't eat the full viewport */
+ .chart-wrap,
+ .chart-wrap > div {
+ height: 240px !important;
+ }
+
+ /* overview-side cards are low priority on mobile — compress them */
+ .overview-side-card.compact {
+ display: none;
+ }
+
+ /* system strip: wrap to 2-per-row on small phones */
+ .overview-system-strip {
+ flex-wrap: wrap;
+ }
+ .overview-system-chip {
+ flex: 0 0 calc(50% - 1px);
+ }
+
+ /* Post row: remove the aside column on very small screens */
+ .post-row-main {
+ grid-template-columns: 28px 1fr;
+ gap: 10px;
+ padding: 12px 14px;
+ }
+}
+
+@media (max-width: 420px) {
+ /* Ultra-narrow: hide ticker entirely — the price display in the overview
+ card covers the same information without crowding the header */
+ .ticker { display: none; }
+}
diff --git a/app/[locale]/glossary/page.tsx b/app/[locale]/glossary/page.tsx
index 1bcd1c0..c2c9f38 100644
--- a/app/[locale]/glossary/page.tsx
+++ b/app/[locale]/glossary/page.tsx
@@ -171,104 +171,105 @@ function getCopy(locale: string): GlossaryCopy {
],
ogTitle: 'Crypto Signals Glossary | Trump Alpha',
ogDescription:
- 'Clear definitions of Trump Alpha’s core terms, optimized for search, AI retrieval, and fast reference.',
+ 'Clear definitions of Trump Alpha\'s core terms, optimized for search, AI retrieval, and fast reference.',
heroTitle: 'Glossary',
heroSubtitle: 'Every term on the platform, defined precisely.',
intro:
- 'Definitions for all the metrics, signal terms, and trading concepts you\'ll encounter on this platform.',
+ 'Quick definitions for the terms you\'ll encounter on this platform.',
terms: [
{
term: 'AHR999',
- category: 'Macro-bottom metric',
+ category: 'Macro-bottom signal',
definition:
- 'AHR999 is a Bitcoin valuation indicator popular in Chinese crypto research. It combines long-term trend and cost-basis style anchors to estimate whether BTC is trading in a deep-value regime. In Trump Alpha, AHR999 below 0.45 counts as one classic bottom signal.',
- extra: 'It is one of the three inputs in the live BTC 2-of-3 macro-bottom scanner.',
+ 'A Bitcoin valuation score. Below 0.45 = BTC is historically cheap and in a potential bottom zone. Above 1.2 = overvalued, bottom thesis is off. One of three conditions in the BTC bottom scanner.',
},
{
term: 'Pi Cycle Bottom',
- category: 'Macro-bottom metric',
+ category: 'Macro-bottom signal',
definition:
- 'Pi Cycle Bottom is a Bitcoin bottom framework based on long-cycle moving-average relationships. In Trump Alpha, the condition is satisfied when the 150-day EMA falls below the 471-day SMA multiplied by 0.745.',
- extra: 'It is one of the three core inputs in the BTC macro-bottom scanner.',
+ 'A long-term moving-average signal that has historically aligned with Bitcoin cycle lows. Fires when the 150-day EMA crosses below the 471-day SMA × 0.745. Binary — either confirmed or not.',
},
{
term: '200-week Moving Average',
- category: 'Macro-bottom metric',
+ category: 'Macro-bottom signal',
definition:
- 'The 200-week moving average is one of Bitcoin’s most widely watched long-term trend anchors. Many historical bear-market lows formed near this zone. Trump Alpha treats price proximity to the 200-week MA as part of BTC bottom confluence.',
+ 'BTC\'s most-watched long-term support level. Every major bear-market low in history formed at or near it. The scanner counts price ≤ 200WMA × 1.05 as a bottom vote.',
},
{
term: 'KOL',
abbr: 'Key Opinion Leader',
category: 'Platform term',
definition:
- 'In crypto, a KOL is an analyst, fund manager, host, or creator whose public views consistently influence market narrative or retail positioning. Trump Alpha emphasizes long-form KOL sources, not only short social fragments.',
+ 'An analyst, fund manager, podcast host, or creator whose calls move crypto narrative. The platform tracks 25 KOLs — Arthur Hayes, Delphi, Bankless, and others — cross-checking what they say publicly against what their wallets actually do.',
},
{
term: 'Talks-vs-Trades Divergence',
category: 'Platform term',
definition:
- 'Talks-vs-trades divergence happens when a KOL’s public stance and wallet behavior point in opposite directions on the same asset. Example: a KOL publishes a bullish ETH thesis while their wallet reduces ETH exposure that same week. Trump Alpha treats the wallet move as the higher-priority truth signal.',
- extra: 'This is one of the platform’s highest-information signals because it separates narrative from actual positioning.',
+ 'When a KOL\'s public call and their wallet move in opposite directions on the same asset. Example: publicly bullish ETH while quietly reducing ETH on-chain. The wallet is treated as the real signal — it\'s harder to fake than words.',
+ extra: 'Highest-conviction signal category on the platform.',
},
{
- term: 'Conviction Score',
+ term: 'Aligned',
category: 'Platform term',
definition:
- 'Conviction Score is the AI-generated strength score assigned to each extracted KOL view, typically ranging from 0.0 to 1.0. Higher values imply clearer language, stronger commitment, and better evidence of timing or sizing intent.',
+ 'A KOL\'s public call and their tracked wallet activity agree — e.g., bullish on BTC and the wallet is adding BTC. Reinforces the signal.',
+ },
+ {
+ term: 'Mismatch',
+ category: 'Platform term',
+ definition:
+ 'A KOL\'s public call contradicts their wallet move. Bullish in public, selling on-chain = mismatch. This is the divergence signal you actually want to act on.',
+ },
+ {
+ term: 'Paper mode',
+ category: 'Platform term',
+ definition:
+ 'Simulated trading — the bot runs through all its logic and tracks positions, but no real orders are sent to Hyperliquid. Use it to see how the bot would have performed before risking real money.',
+ },
+ {
+ term: '/adopt',
+ category: 'Platform term',
+ definition:
+ 'A Telegram bot command. When a Macro Vibes signal fires, you open the position yourself on Hyperliquid, then send /adopt in the bot. The bot then takes over managing the exit — stop ladder, partial de-risk, pyramiding — without you having to watch it.',
+ },
+ {
+ term: 'AI Confidence',
+ category: 'Platform term',
+ definition:
+ 'A percentage score (0–100%) the AI assigns to each Trump post signal. Higher = the post language is clearer and more explicitly directional. The default threshold is 70% — signals below this are not traded.',
},
{
term: 'Funding Rate',
category: 'Derivatives term',
definition:
- 'Funding rate is the periodic payment mechanism used by perpetual futures to keep contract pricing anchored to spot. If longs pay shorts, the market is long-crowded. If shorts pay longs, the market is short-crowded. Extreme readings often precede liquidation-driven reversals.',
+ 'The periodic payment between long and short traders on a perp exchange, designed to keep the contract price close to spot. Extreme positive funding = crowded longs. Extreme negative = crowded shorts. Extreme readings often precede sharp reversals when the crowded side gets squeezed.',
},
{
term: 'Isolated Margin',
category: 'Trading term',
definition:
- 'Isolated margin means each trade uses its own dedicated margin rather than sharing risk with the rest of the account. If one position is liquidated, it does not automatically cascade into other positions.',
- extra: 'Trump Alpha’s execution layer is designed around isolated margin.',
+ 'Each trade uses only its own dedicated margin — if it gets liquidated, it doesn\'t touch your other positions. Trump Alpha always uses isolated margin so one bad trade can\'t wipe the account.',
},
{
term: 'TP / SL',
abbr: 'Take-Profit / Stop-Loss',
category: 'Trading term',
definition:
- 'TP and SL are conditional exit orders for profit-taking and loss control. Trump Alpha’s auto-trader attaches both at entry rather than after the position is already open.',
- },
- {
- term: 'UTXO',
- abbr: 'Unspent Transaction Output',
- category: 'Bitcoin term',
- definition:
- 'UTXO is the fundamental accounting unit of the Bitcoin network. Trump Alpha’s live Macro Vibes module no longer relies directly on UTXO-spend behavior, but the concept still matters for interpreting classic on-chain research.',
+ 'Automatic exit orders placed at entry. TP closes the trade in profit at your target. SL closes it to cut losses. Trump Alpha attaches both the moment a trade opens.',
},
{
term: 'Perpetual Future',
abbr: 'Perp',
category: 'Derivatives term',
definition:
- 'A perp is a futures contract with no expiry date. It stays open until the trader closes it or gets liquidated, while funding rate and mark-price mechanisms help keep it close to spot. Hyperliquid is one example of a perp venue.',
- },
- {
- term: 'Substack Signal',
- category: 'Platform term',
- definition:
- 'A Substack signal is an extracted asset view taken from a KOL’s long-form essay, newsletter, or blog post. These sources usually contain fuller reasoning than short posts and therefore produce more retrieval-friendly signal data.',
+ 'A futures contract with no expiry. You stay in the trade until you close it or get liquidated. Trump Alpha executes on Hyperliquid perps.',
},
{
term: 'Capitulation',
category: 'Market term',
definition:
- 'Capitulation is the stage of a drawdown when holders finally give up and sell into weakness. It often appears near the end of a bear market and is one of the environments the BTC bottom scanner is designed to detect.',
- },
- {
- term: 'EMA',
- abbr: 'Exponential Moving Average',
- category: 'Technical term',
- definition:
- 'EMA is a moving average that gives more weight to recent price data than older data. Trump Alpha uses the 150-day EMA inside the Pi Cycle Bottom condition.',
+ 'The phase near a bear market bottom where panicked holders give up and sell en masse. Creates the washout conditions that the BTC macro-bottom scanner is designed to detect.',
},
],
footerLead: 'Missing a term?',
diff --git a/app/[locale]/kol/KolPageClient.tsx b/app/[locale]/kol/KolPageClient.tsx
index 4525ff3..c21da3f 100644
--- a/app/[locale]/kol/KolPageClient.tsx
+++ b/app/[locale]/kol/KolPageClient.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useEffect, useMemo, useState } from 'react'
+import { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useLocale } from 'next-intl'
import type {
@@ -42,8 +42,8 @@ function actionLabel(action: KolTicker['action'], isZh: boolean) {
}
function windowLabel(days: number, isZh: boolean) {
- if (days === 1) return isZh ? '今日' : 'Today'
- return isZh ? `近 ${days} 日` : `Last ${days}d`
+ if (days === 1) return 'Today'
+ return `Last ${days}d`
}
function changeLabel(changeType: KolHoldingChange['change_type'], isZh: boolean) {
@@ -100,6 +100,7 @@ function chainActionLabel(action: string, isZh: boolean) {
function sourceLabel(source: string, isZh: boolean) {
if (source === 'substack') return 'Substack'
+ if (source === 'blog') return 'Blog'
if (source === 'podcast') return 'Podcast'
if (source === 'twitter') return 'X'
return source
@@ -138,19 +139,20 @@ function DigestWidget({
activeTicker,
isZh,
initialDigest = null,
+ days,
}: {
onTickerClick: (ticker: string) => void
activeTicker?: string | null
isZh: boolean
initialDigest?: KolDigest | null
+ days: number
}) {
- const [days, setDays] = useState
(7)
const [data, setData] = useState(initialDigest)
const [loading, setLoading] = useState(initialDigest === null)
const [err, setErr] = useState('')
useEffect(() => {
- if (initialDigest === null || days !== (initialDigest?.window_days ?? 7)) {
+ if (initialDigest === null || days !== (initialDigest?.window_days ?? 30)) {
setLoading(true)
}
swrFetch(
@@ -160,64 +162,34 @@ function DigestWidget({
fresh => setData(fresh),
)
.then(d => { setData(d); setErr('') })
- .catch(e => setErr(e instanceof Error ? e.message : (isZh ? '加载失败' : 'Failed to load digest')))
+ .catch(e => setErr(e instanceof Error ? e.message : ('Failed to load digest')))
.finally(() => setLoading(false))
}, [days, initialDigest, isZh])
return (
-
-
-
-
- What KOLs are pushing now
-
-
- {data
- ? `${data.post_count} posts analysed · ${data.ticker_count} assets with repeat mentions`
- : 'Loading…'}
-
+
+ {/* Compact meta line — no header label needed, context is obvious */}
+ {data && !loading && (
+
+ {data.post_count} posts · {data.ticker_count} assets
-
- {WINDOW_OPTIONS.map(daysOption => (
- setDays(daysOption)}
- className={`nav-tab ${days === daysOption ? 'active' : ''}`}
- style={{ border: 'none', cursor: 'pointer', fontSize: 12 }}
- >
- {windowLabel(daysOption, isZh)}
-
- ))}
-
-
-
+ )}
{loading && (
-
Loading…
+
Loading…
)}
{err && (
{err}
)}
{!loading && !err && data && data.tickers.length === 0 && (
-
- No actionable calls in this window. Try a longer range.
+
+ No repeat mentions in this window.
)}
{!loading && !err && data && data.tickers.length > 0 && (
{data.tickers.map(t =>
-
+
{t.ticker}
{active && (
@@ -294,11 +266,10 @@ function DigestTickerChip({
{actionLabel}
-
- {digestSideCopy(t.side)}
-
+ {/* Compact: KOL count + handles in two lines, no "Bullish flow" copy
+ (Long/Short label already conveys direction). */}
- {t.kol_count} KOL{t.kol_count !== 1 ? 's' : ''} · {(t.max_conviction * 100).toFixed(0)}% max conviction
+ {t.kol_count} KOL{t.kol_count !== 1 ? 's' : ''} · {(t.max_conviction * 100).toFixed(0)}%
{
+ const gen = ++walletCheckGenRef.current
setLoading(true)
Promise.all([
swrFetch(
`kol-digest-${days}`,
15 * 60_000,
() => getKolDigest(days),
- fresh => setDigest(fresh),
+ fresh => { if (gen === walletCheckGenRef.current) setDigest(fresh) },
),
swrFetch(
`kol-changes-${days}`,
30 * 60_000,
() => getKolChanges({ days }),
- fresh => setChanges(fresh.changes),
+ fresh => { if (gen === walletCheckGenRef.current) setChanges(fresh.changes) },
),
swrFetch(
`kol-divergence-all-${days}`,
30 * 60_000,
() => getKolDivergence({ days }),
- fresh => setItems(fresh.items),
+ fresh => { if (gen === walletCheckGenRef.current) setItems(fresh.items) },
),
])
.then(([nextDigest, nextChanges, nextItems]) => {
+ if (gen !== walletCheckGenRef.current) return
setDigest(nextDigest)
setChanges(nextChanges.changes)
setItems(nextItems.items)
})
.catch(() => {
+ if (gen !== walletCheckGenRef.current) return
setDigest(null)
setChanges([])
setItems([])
})
- .finally(() => setLoading(false))
+ .finally(() => { if (gen === walletCheckGenRef.current) setLoading(false) })
}, [days])
const rows = useMemo(() => {
@@ -458,43 +438,21 @@ function WalletCheckWidget({
borderRadius: 12, background: 'var(--surface)',
border: '1px solid var(--line)',
}}>
-
-
-
- Talks vs wallets
-
-
- {loading
- ? 'Loading…'
- : rows.length === 0
- ? 'No overlapping talk and wallet evidence in this window.'
- : `${totalAligned} aligned · ${totalMismatch} mismatched across the assets KOLs are pushing`}
-
+ {/* Compact summary — no uppercase header, no redundant label */}
+ {!loading && rows.length > 0 && (
+
+ {totalAligned > 0 && ✓ {totalAligned} aligned }
+ {totalMismatch > 0 && ⚠ {totalMismatch} mismatch }
+ {totalAligned === 0 && totalMismatch === 0 && 'Wallet evidence pending'}
-
- {([1, 7, 30] as const).map(d => (
- setDays(d)}
- className={`nav-tab ${days === d ? 'active' : ''}`}
- style={{ border: 'none', cursor: 'pointer', fontSize: 12 }}
- >
- {windowLabel(d, isZh)}
-
- ))}
-
-
+ )}
{!loading && rows.length === 0 && (
- When KOLs call an asset, do tracked wallets back it up? No overlap yet in this window — widen the range or check back after the next feed run.
+ No wallet overlap for this window — try a wider range.
)}
@@ -521,7 +479,7 @@ function WalletCheckWidget({
{row.verdict.label}
-
+
{row.talkLine}
@@ -533,10 +491,8 @@ function WalletCheckWidget({
-
- Wallet evidence
-
-
+ {/* No "WALLET EVIDENCE" label — context is clear from layout */}
+
{row.latestMatch
? `@${row.latestMatch.handle} ${chainActionLabel(row.latestMatch.onchain_action, isZh).toLowerCase()}`
: row.latestChange
@@ -544,9 +500,13 @@ function WalletCheckWidget({
: 'No tracked move yet'}
- {row.latestMatch
- ? `${row.verdict.note} ${row.latestMatch.usd_after ? `Size ${formatShortUsd(row.latestMatch.usd_after)}.` : ''}`
- : row.verdict.note}
+ {/* Drop boilerplate "Wallet action supports/disagrees the public pitch." —
+ the verdict badge already says Aligned / Mismatch. Just show the size. */}
+ {row.latestMatch?.usd_after
+ ? `Size ${formatShortUsd(row.latestMatch.usd_after)}`
+ : row.verdict.label === 'No wallet proof'
+ ? 'No tracked wallet move in this window'
+ : ''}
{(row.divergenceCount > 0 || row.alignmentCount > 0) && (
@@ -660,7 +620,7 @@ function PostDetail({
margin: 0, fontSize: 'clamp(16px, 4.5vw, 22px)',
lineHeight: 1.3, wordBreak: 'break-word',
}}>
- {post.title || (isZh ? '(无标题)' : '(Untitled)')}
+ {post.title || post.summary || (post.source === 'twitter' ? '(Tweet)' : '(Untitled)')}
@@ -679,7 +639,7 @@ function PostDetail({
whiteSpace: 'nowrap',
}}
>
- {isZh ? `查看原帖 ${postSourceLabel}` : `Open original ${postSourceLabel}`} ↗
+ {`Open original ${postSourceLabel}`} ↗
)}
- {isZh ? 'AI 摘要' : 'AI summary'}
+ {'AI summary'}
{post.summary}
@@ -714,7 +674,7 @@ function PostDetail({
- {isZh ? '提取标的' : 'Extracted assets'}
+ {'Extracted assets'}
{post.tickers.map((t, i) => (
@@ -729,7 +689,7 @@ function PostDetail({
{actionLabel(t.action, isZh)}
- {isZh ? '信心分' : 'Conviction'} {(t.conviction * 100).toFixed(0)}%
+ {'Conviction'} {(t.conviction * 100).toFixed(0)}%
- {isZh ? '原文摘录' : 'Source excerpt'}
+ {'Source excerpt'}
(initialPosts ?? [])
- const [loading, setLoading] = useState(initialPosts === null)
- const [err, setErr] = useState('')
- const [openPost, setOpenPost] = useState
(null)
- const [handleFilter, setHandleFilter] = useState('all')
- const [tickerFilter, setTickerFilter] = useState(null)
- const [kolPage, setKolPage] = useState(1)
+ const dateLocale = 'en-US'
+ const isZh = false // i18n shelved — passed to helper fns that still take the param
+ const [posts, setPosts] = useState(initialPosts ?? [])
+ const [serverTotal, setServerTotal] = useState(initialTotal)
+ const [loading, setLoading] = useState(initialPosts === null)
+ const [err, setErr] = useState('')
+ const [openPost, setOpenPost] = useState(null)
+ const [sourceFilter, setSourceFilter] = useState('all')
+ const [signalsOnly, setSignalsOnly] = useState(false)
+ const [tickerFilter, setTickerFilter] = useState(null)
+ const [serverPage, setServerPage] = useState(1)
+ // Unified time window — controls both DigestWidget and WalletCheckWidget.
+ // Default 30d, not 7d: KOL ingestion is daily and sparse, so a 7d window is
+ // frequently empty (e.g. 7d=0 while 30d=196) and the first paint showed
+ // "No data yet" for modules that actually have data.
+ const [kolDays, setKolDays] = useState(30)
+
+ const postsGenRef = useRef(0)
useEffect(() => {
+ const gen = ++postsGenRef.current
+ setLoading(true)
+ const src = sourceFilter === 'all' ? undefined : sourceFilter
+ const cacheKey = `kol-posts-${sourceFilter}-${signalsOnly}-${serverPage}-${tickerFilter ?? ''}-${kolDays}`
swrFetch(
- 'kol-posts-100',
+ cacheKey,
15 * 60_000, // 15 min TTL — KOL feed is ingested daily
- () => getKolPosts({ limit: 100 }),
- fresh => setPosts(fresh.items),
+ () => getKolPosts({
+ limit: KOL_SERVER_PAGE_SIZE, page: serverPage, source: src, signalsOnly,
+ ticker: tickerFilter ?? undefined, days: kolDays,
+ }),
+ fresh => { if (gen === postsGenRef.current) { setPosts(fresh.items); setServerTotal(fresh.total ?? 0) } },
)
- .then(r => { setPosts(r.items); setErr('') })
- .catch(e => setErr(e instanceof Error ? e.message : (isZh ? '加载失败' : 'Failed to load posts')))
- .finally(() => setLoading(false))
- }, [isZh])
+ .then(r => {
+ if (gen !== postsGenRef.current) return
+ setPosts(r.items); setServerTotal(r.total ?? 0); setErr('')
+ })
+ .catch(e => { if (gen === postsGenRef.current) setErr(e instanceof Error ? e.message : ('Failed to load posts')) })
+ .finally(() => { if (gen === postsGenRef.current) setLoading(false) })
+ }, [sourceFilter, signalsOnly, serverPage, tickerFilter, kolDays])
- const handles = useMemo(() => {
- const set = new Set(posts.map(p => p.kol_handle))
- return ['all', ...Array.from(set)]
- }, [posts])
+ // posts are already filtered server-side; no client-side re-filter needed
+ const filtered = posts
- const filtered = useMemo(() => {
- let out = handleFilter === 'all' ? posts : posts.filter(p => p.kol_handle === handleFilter)
- if (tickerFilter) {
- out = out.filter(p => p.tickers.some(t => t.ticker === tickerFilter))
- }
- return out
- }, [posts, handleFilter, tickerFilter])
-
- const kolTotalPages = Math.max(1, Math.ceil(filtered.length / KOL_PAGE_SIZE))
- const kolSafePage = Math.min(kolPage, kolTotalPages)
- const kolPageItems = filtered.slice((kolSafePage - 1) * KOL_PAGE_SIZE, kolSafePage * KOL_PAGE_SIZE)
+ const totalServerPages = Math.max(1, Math.ceil(serverTotal / KOL_SERVER_PAGE_SIZE))
async function openDetail(id: number) {
try {
const detail = await getKolPost(id)
setOpenPost(detail)
} catch (e) {
- setErr(e instanceof Error ? e.message : (isZh ? '详情加载失败' : 'Failed to load detail'))
+ setErr(e instanceof Error ? e.message : ('Failed to load detail'))
}
}
+ function changeSource(src: SourceFilter) {
+ setSourceFilter(src)
+ setServerPage(1)
+ setTickerFilter(null)
+ }
+
return (
-
{isZh ? 'KOL 信号' : 'KOL Signals'}
-
- Which assets KOLs are pushing right now — and whether tracked wallets back it up or call their bluff.
+ {'KOL Signals'}
+
+ Arthur Hayes, Delphi, Bankless, and 22 more — their public calls vs what their wallets actually do.
+ {/* Single time filter controlling both widgets */}
+
+ {WINDOW_OPTIONS.map(d => (
+ { setKolDays(d); setServerPage(1) }}
+ className={`nav-tab ${kolDays === d ? 'active' : ''}`}
+ style={{ border: 'none', cursor: 'pointer', fontSize: 12 }}
+ >
+ {windowLabel(d, isZh)}
+
+ ))}
+
+
+
+ {/* Source filter — All / Substack / X + Signals only toggle */}
+
+ {(['all', 'substack', 'blog', 'podcast', 'twitter'] as SourceFilter[]).map(src => {
+ const label = src === 'all' ? 'All' : src === 'substack' ? 'Substack' : src === 'blog' ? 'Blog' : src === 'podcast' ? 'Podcast' : 'X (Twitter)'
+ const active = sourceFilter === src
+ return (
+ changeSource(src)}
+ style={{
+ padding: '4px 12px', borderRadius: 6, fontSize: 12, fontWeight: 600,
+ cursor: 'pointer', border: '1px solid var(--line)',
+ background: active ? 'var(--amber)' : 'var(--surface)',
+ color: active ? '#000' : 'var(--ink-2)',
+ transition: 'background 0.15s, color 0.15s',
+ }}
+ >
+ {label}
+
+ )
+ })}
+ { setSignalsOnly(v => !v); setServerPage(1) }}
+ title="Exclude noise posts (short tweets, gm, RT). Long-form and directional posts are kept."
+ style={{
+ marginLeft: 8,
+ padding: '4px 12px', borderRadius: 6, fontSize: 12, fontWeight: 600,
+ cursor: 'pointer',
+ border: `1px solid ${signalsOnly ? '#16a34a88' : 'var(--line)'}`,
+ background: signalsOnly ? '#16a34a22' : 'var(--surface)',
+ color: signalsOnly ? '#16a34a' : 'var(--ink-3)',
+ transition: 'background 0.15s, color 0.15s, border-color 0.15s',
+ }}
+ >
+ {signalsOnly ? '✓ Signals only' : 'Signals only'}
+
{
setTickerFilter(prev => prev === sym ? null : sym)
- setKolPage(1)
+ setServerPage(1)
}}
/>
@@ -858,6 +886,7 @@ export default function KolPage({
initialChanges={initialChanges}
initialItems={initialDivergence}
tickerFilter={tickerFilter}
+ days={kolDays}
/>
{tickerFilter && (
@@ -865,47 +894,38 @@ export default function KolPage({
display: 'flex', alignItems: 'center', gap: 8,
marginBottom: 12, fontSize: 12,
}}>
- {isZh ? '当前筛选标的:' : 'Filtered asset:'}
+ {'Filtered asset:'}
{tickerFilter}
{ setTickerFilter(null); setKolPage(1) }}
+ onClick={() => { setTickerFilter(null); setServerPage(1) }}
style={{
background: 'var(--bg-sunk)', border: '1px solid var(--line)',
borderRadius: 4, padding: '2px 8px',
cursor: 'pointer', fontSize: 11, color: 'var(--ink-2)',
}}
- >{isZh ? '清除 ✕' : 'Clear ✕'}
+ >{'Clear ✕'}
)}
- {handles.length > 2 && (
-
- {handles.map(h => (
- { setHandleFilter(h); setKolPage(1) }}
- className={`nav-tab ${handleFilter === h ? 'active' : ''}`}
- style={{ border: 'none', cursor: 'pointer' }}
- >
- {h === 'all' ? (isZh ? `全部 (${posts.length})` : `All (${posts.length})`) : `@${h}`}
-
- ))}
-
- )}
+ {/* KOL handle filter removed — 25 handles as a horizontal tab bar
+ overflows on every screen size and most users filter by asset (ticker),
+ not by person. Handle is visible on each post card. */}
+
+
+ All posts · paginated feed
+
{loading && }
- {err && {isZh ? `错误:${err}` : `Error: ${err}`}
}
+ {err && {`Error: ${err}`}
}
{!loading && !err && (
{filtered.length === 0 && (
- {isZh
- ? '当前没有数据,等待下一轮抓取任务(每天 01:15 UTC)。'
- : 'No data yet. Wait for the next feed ingestion run (daily 01:15 UTC).'}
+ {'No data yet. Wait for the next feed ingestion run (daily 01:15 UTC).'}
)}
- {kolPageItems.map(p => (
+ {filtered.map(p => (
openDetail(p.id)}
@@ -927,10 +947,44 @@ export default function KolPage({
@{p.kol_handle} · {sourceLabel(p.source, isZh)} · {new Date(p.published_at).toLocaleDateString(dateLocale)}
+ {p.tier === 'trade_signal' && (
+
+ SIGNAL
+
+ )}
+ {p.tier === 'directional' && (
+
+ VIEW
+
+ )}
+ {p.talks_vs_trades_flag && (
+
+ ⚠ FLIP
+
+ )}
{p.analyzed_at
- ? (isZh ? '✓ 已分析' : '✓ Analyzed')
- : (isZh ? '⏳ 待分析' : '⏳ Pending')}
+ ? ('✓ Analyzed')
+ : ('⏳ Pending')}
{p.url && (
- {isZh ? '原帖 ↗' : 'Source ↗'}
+ {'Source ↗'}
)}
- {p.title || (isZh ? '(无标题)' : '(Untitled)')}
+ {p.title || p.summary || (p.source === 'twitter' ? '(Tweet)' : '(Untitled)')}
- {p.summary && (
+ {/* Only show summary as a separate line when there's a real title above it */}
+ {p.title && p.summary && (
{p.summary}
@@ -966,11 +1021,11 @@ export default function KolPage({
))}
diff --git a/app/[locale]/kol/loading.tsx b/app/[locale]/kol/loading.tsx
new file mode 100644
index 0000000..013c367
--- /dev/null
+++ b/app/[locale]/kol/loading.tsx
@@ -0,0 +1,27 @@
+/** Instant skeleton while KolPage fetches posts and digest. */
+export default function KolLoading() {
+ return (
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+ )
+}
diff --git a/app/[locale]/kol/page.tsx b/app/[locale]/kol/page.tsx
index 875cf52..3257d23 100644
--- a/app/[locale]/kol/page.tsx
+++ b/app/[locale]/kol/page.tsx
@@ -19,8 +19,8 @@ export async function generateMetadata({
return {
title: isZh ? 'KOL 信号与言行偏离追踪' : 'KOL Signals & Talks-vs-Trades Divergence',
description: isZh
- ? '从 15 个加密 KOL 信息源中提取可执行观点,覆盖 Arthur Hayes、Delphi Digital、Bankless、Empire、Unchained 等,并追踪“说法”和链上仓位是否一致。'
- : 'AI-extracted crypto signals from 19 KOL feeds: Arthur Hayes, Delphi Digital, Bankless, Empire, Unchained and more. Includes talks-vs-trades divergence — when KOL wallets contradict their public posts.',
+ ? '从 25 个加密 KOL 信息源中提取可执行观点,覆盖 Arthur Hayes、Delphi Digital、Bankless、Empire、Unchained 等,并追踪“说法”和链上仓位是否一致。'
+ : 'AI-extracted crypto signals from 25 KOL feeds: Arthur Hayes, Delphi Digital, Bankless, Empire, Unchained and more. Includes talks-vs-trades divergence — when KOL wallets contradict their public posts.',
keywords: isZh
? [
'KOL 加密信号',
@@ -48,7 +48,7 @@ export async function generateMetadata({
title: isZh ? 'KOL 信号与言行偏离 | Trump Alpha' : 'KOL Signals & Talks-vs-Trades | Trump Alpha',
description: isZh
? '每天分析 KOL 长文、播客和公开观点,再与链上仓位交叉验证,找出真正有信息增量的观点与偏离。'
- : '19 KOL feeds (Hayes, Bankless, Empire…) AI-scored daily. Plus talks-vs-trades: when their wallets contradict their words.',
+ : '25 KOL feeds (Hayes, Bankless, Empire…) AI-scored daily. Plus talks-vs-trades: when their wallets contradict their words.',
},
alternates: {
canonical: `${siteUrl}/en/kol`,
@@ -60,7 +60,7 @@ export async function generateMetadata({
}
}
-// GEO: the KOL talks-vs-trades feed is a dataset cross-referencing 19 crypto
+// GEO: the KOL talks-vs-trades feed is a dataset cross-referencing 25 crypto
// KOLs' public statements against their on-chain wallet behaviour.
const kolDataset = {
'@context': 'https://schema.org',
@@ -68,7 +68,7 @@ const kolDataset = {
'@id': `${siteUrl}/en/kol#dataset`,
name: 'Crypto KOL talks-vs-trades divergence dataset',
description:
- 'Daily cross-reference of 19 crypto KOLs\' public statements (Substack / podcast) against their on-chain Ethereum wallet activity, flagging divergence when public stance and real positioning disagree within a ±7-day window.',
+ 'Daily cross-reference of 25 crypto KOLs\' public statements (Substack / podcast) against their on-chain Ethereum wallet activity, flagging divergence when public stance and real positioning disagree within a ±7-day window.',
url: `${siteUrl}/en/kol`,
keywords: ['crypto KOL', 'on-chain', 'divergence', 'wallet tracking', 'sentiment'],
isAccessibleForFree: true,
@@ -80,10 +80,13 @@ const kolDataset = {
}
export default async function KolPage() {
+ // days=30 must match the client default (KolPageClient kolDays=30) so the
+ // SSR payload and first client render agree — otherwise content flashes on
+ // mount. 7d is frequently empty while 30d has data.
const [posts, digest, changes, divergence] = await Promise.all([
- getKolPosts({ limit: 100 }).catch(() => null),
- getKolDigest(7).catch(() => null),
- getKolChanges({ days: 7 }).catch(() => null),
+ getKolPosts({ limit: 50, page: 1, days: 30 }).catch(() => null),
+ getKolDigest(30).catch(() => null),
+ getKolChanges({ days: 30 }).catch(() => null),
getKolDivergence({ days: 30 }).catch(() => null),
])
@@ -96,6 +99,7 @@ export default async function KolPage() {
+
{children}
import('@/components/btc/MacroPanel'), {
@@ -84,70 +84,63 @@ export default function MacroVibesPage({
-
{isZh ? '宏观氛围' : 'Macro Vibes'}
- {/* PageHint: brand tagline for the page — same across both tabs so
- the "what is this page" answer doesn't shift under the user when
- they click between Bottom / Funding. Tab-specific notes live in
- the section-hint card right below the tab bar. */}
-
- Macro vibes for crypto. Read what's about to happen before price prints it.
-
+
+
{isZh ? '宏观氛围' : 'Macro Vibes'}
+
+ {isZh ? '手动开仓 · Bot 托管' : 'You open · bot manages'}
+
+
Live
-
- setTab('bottom')}
- className={`nav-tab ${tab === 'bottom' ? 'active' : ''}`}
- style={{ border: 'none', cursor: 'pointer' }}
- >
- {isZh ? '周期底部' : 'Macro Bottom'}
-
- setTab('funding')}
- className={`nav-tab ${tab === 'funding' ? 'active' : ''}`}
- style={{ border: 'none', cursor: 'pointer' }}
- >
- {isZh ? '资金费率反转' : 'Funding Reversal'}
-
-
-
- {/* Always-visible per-tab explanation. Was hidden behind a hover
- tooltip on the tab button — users couldn't tell what they were
- about to look at until they explicitly hovered. */}
- {tab === 'bottom' && (
-
- Fires when
≥2 of 3 classic bottom signals agree:{' '}
-
AHR999 < 0.45 ·
price ≤ 200-week MA ·
Pi Cycle Bottom .
- {' '}Long-only · 2–4 fires per cycle · trailing-stop exit · max hold ~18 months.
+ {/* Tabs + inline description on the same row — avoids a separate
+ "section hint" block that adds a third layer before the data. */}
+
+
+ setTab('bottom')}
+ className={`nav-tab ${tab === 'bottom' ? 'active' : ''}`}
+ style={{ border: 'none', cursor: 'pointer' }}
+ >
+ {isZh ? '周期底部' : 'Macro Bottom'}
+
+ setTab('funding')}
+ className={`nav-tab ${tab === 'funding' ? 'active' : ''}`}
+ style={{ border: 'none', cursor: 'pointer' }}
+ >
+ {isZh ? '资金费率反转' : 'Funding Reversal'}
+
- )}
+
+ {tab === 'bottom'
+ ? <>≥2 of 3: AHR999 < 0.45 · 200w MA · Pi Cycle Bottom — long-only, rare.>
+ : <>Fades crowded perps when 30d cumulative funding crosses ±3% and cools. Hourly.>}
+
+
{/* Macro indicator panel — only relevant on the Macro Bottom tab where
users are reasoning about the broader risk regime. The Funding tab
has its own live funding panel below. */}
{tab === 'bottom' &&
}
{tab === 'funding' && (
-
- Mean-reversion against crowded perp positioning. When 30-day cumulative funding crosses{' '}
- ±3% and recent cycles start cooling, the scanner fades the crowded side.
- Checked hourly.
-
- )}
-
- {/* Old plain-text per-tab description removed — the same content
- now lives in the section-hint block above the tab content. */}
-
- {tab === 'funding' && (
-
+ <>
+
+
+ >
)}
@@ -179,13 +172,11 @@ export default function MacroVibesPage({
)}
{!loading && !loadErr && filtered.length === 0 && (
- {tab === 'bottom'
- ? 'No macro-bottom signals yet.'
- : 'No funding-reversal signals yet.'}
-
+ {tab === 'bottom' ? 'No bottom signals yet.' : 'No funding signals yet.'}
+
{tab === 'bottom'
- ? 'This scanner is intentionally rare — only fires at genuine macro bottoms (daily 00:45 UTC).'
- : 'Fires when 30-day cumulative funding exceeds ±3% AND starts mean-reverting. See the live panel above for current state.'}
+ ? 'Intentionally rare — fires only at genuine cycle bottoms.'
+ : 'Check the live panel above for current funding state.'}
)}
diff --git a/app/[locale]/macro/loading.tsx b/app/[locale]/macro/loading.tsx
new file mode 100644
index 0000000..c0f3791
--- /dev/null
+++ b/app/[locale]/macro/loading.tsx
@@ -0,0 +1,29 @@
+/** Instant skeleton while MacroVibesPage fetches indicator data. */
+export default function MacroLoading() {
+ return (
+
+
+ {/* Composite score card */}
+
+ {/* 8-indicator grid */}
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+
+ )
+}
diff --git a/app/[locale]/macro/page.tsx b/app/[locale]/macro/page.tsx
index d371f62..102d2cb 100644
--- a/app/[locale]/macro/page.tsx
+++ b/app/[locale]/macro/page.tsx
@@ -64,7 +64,10 @@ export async function generateMetadata({
export default async function MacroVibesPage() {
const [posts, fundingSnapshot] = await Promise.all([
- getPosts(500, 1).catch(() => null),
+ // The default tab is the bottom-reversal view. Fetch only the source this
+ // page actually renders on first paint instead of hydrating with the
+ // latest global post firehose and filtering it client-side.
+ getPosts(200, 1, 'btc_bottom_reversal').catch(() => null),
getFundingSnapshot().catch(() => null),
])
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index 2be1fb7..728a351 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -19,7 +19,7 @@ export async function generateMetadata({ params }: OverviewPageProps): Promise
[])
+ // The overview only renders a small curated slice on first paint; pulling
+ // 500 posts here bloats the server payload without improving the initial UI.
+ const posts = await getPosts(80, 1).catch(() => [])
return (
import('@/components/trades/BotConfigPanel'), {
ssr: false,
loading: () =>
,
})
-/**
- * Settings page — the home for all configuration.
- *
- * Previously the BotConfigPanel lived on /trades (alongside the trade history),
- * and /settings was just a redirect card. That made /trades double-duty
- * (configure AND view results) and /settings feel like a dead end. We swap:
- * - /settings → all configuration (bot, exchange key, subscription)
- * - /trades → pure execution view (open positions + history)
- * Legal links stay here since this is the "everything else" page.
- */
export default function SettingsClient() {
- const localeIntl = useLocale()
- const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
- const { address, isConnected } = useAccount()
- const [mounted, setMounted] = useState(false)
const pathname = usePathname()
-
- useEffect(() => { setMounted(true) }, [])
const locale = pathname.split('/')[1] || 'en'
const href = (path: string) => `/${locale}${path}`
- const walletLabel = mounted && isConnected && address
- ? `${address.slice(0, 6)}…${address.slice(-4)}`
- : 'Not connected'
return (
<>
-
-
-
Control center
-
One place to arm, limit, and verify the bot
-
- Arm the bot, set per-system risk limits, and configure Telegram alerts — all in one place.
-
-
-
-
- Wallet
- {walletLabel}
-
-
- Private data
- {mounted && isConnected ? 'Ready to unlock' : 'Connect first'}
-
-
- Main actions
- Load settings, save limits, link API
-
-
-
-
-
-
- Your account's full control surface — subscription, Hyperliquid API key, risk limits, and alert delivery.
-
-
- Trump drives entries · Macro Vibes manages BTC · Global limits apply to both.
-
-
-
-
-
- Trump Signal
- Event-driven entry settings
-
- Position size, Trump leverage, and minimum AI confidence used when a Truth Social post becomes actionable.
-
- Configure Trump ↓
-
-
-
- Macro Vibes
- BTC manage-only settings
-
- Strategy mode, BTC leverage, and de-risk targets for the Macro Vibes system.
-
- Configure Macro Vibes ↓
-
-
-
- Global
- Account-wide execution controls
-
- Subscription plan, Hyperliquid API key, trading schedule, and guardrails that apply to both systems.
-
- Configure global ↓
-
-
-
- {/* Account card — quick "who am I logged in as" */}
-
-
- {isZh ? '账户' : 'Account'}
-
-
-
- {mounted && isConnected ? '✓' : '○'}
-
-
-
- {mounted && isConnected && address
- ? {address.slice(0, 10)}…{address.slice(-8)}
- : (isZh ? '未连接' : 'Not connected')}
-
-
- {mounted && isConnected ? (isZh ? '钱包是下方所有配置的身份入口' : 'Wallet is the access key for everything below') : (isZh ? '连接钱包后才能配置机器人' : 'Connect a wallet to configure the bot')}
-
-
-
-
-
-
-
-
-
Execution setup
-
Trading permissions and risk controls
-
-
- Load once, then adjust by module without leaving the page.
-
-
-
- {/* The full bot config UI (subscribe, HL key, risk settings, schedule) */}
+
-
-
-
-
Delivery
-
Telegram alerts
-
-
- Set up Telegram alerts once the bot is live.
-
-
+
-
-
-
-
Support
-
Legal and contact
-
-
-
- {/* Legal & support */}
-
-
- {isZh ? '法律与支持' : 'Legal & support'}
-
-
- {isZh ? '隐私政策 →' : 'Privacy Policy →'}
- {isZh ? '服务条款 →' : 'Terms of Service →'}
- {isZh ? '联系我们 →' : 'Contact Us →'}
-
+
+
+ Privacy Policy →
+ Terms →
+ Contact →
>
diff --git a/app/[locale]/settings/loading.tsx b/app/[locale]/settings/loading.tsx
new file mode 100644
index 0000000..091a9f5
--- /dev/null
+++ b/app/[locale]/settings/loading.tsx
@@ -0,0 +1,26 @@
+/** Instant skeleton while SettingsPage loads. */
+export default function SettingsLoading() {
+ return (
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+
+
+ {Array.from({ length: 3 }).map((_, j) => (
+
+ ))}
+
+
+ ))}
+
+ )
+}
diff --git a/app/[locale]/trades/TradesPageClient.tsx b/app/[locale]/trades/TradesPageClient.tsx
index 9f5a8c8..53dd343 100644
--- a/app/[locale]/trades/TradesPageClient.tsx
+++ b/app/[locale]/trades/TradesPageClient.tsx
@@ -5,8 +5,8 @@ import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useLocale } from 'next-intl'
import { useAccount, useSignMessage } from 'wagmi'
-import type { BotTrade, TrumpPost } from '@/types'
-import { getTrades, getPosts } from '@/lib/api'
+import type { BotTrade } from '@/types'
+import { getTrades, getUserPublic } from '@/lib/api'
import { useDashboardStore } from '@/store/dashboard'
import { getCachedViewEnvelope, getOrCreateViewEnvelope, type SignedEnvelope } from '@/lib/signedRequest'
import TradeTable from '@/components/trades/TradeTable'
@@ -18,13 +18,13 @@ export default function TradesPageClient() {
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
const { address, isConnected } = useAccount()
const { signMessageAsync } = useSignMessage()
- const { isSubscribed, hlApiKeySet } = useDashboardStore()
+ const { isSubscribed, hlApiKeySet, paperMode,
+ setSubscribed, setHlApiKeySet, setPaperMode, setBotReadiness } = useDashboardStore()
const pathname = usePathname()
const locale = pathname.split('/')[1] || 'en'
const [mounted, setMounted] = useState(false)
const [trades, setTrades] = useState
([])
- const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [loadErr, setLoadErr] = useState('')
const [needsUnlock, setNeedsUnlock] = useState(false)
@@ -37,41 +37,77 @@ export default function TradesPageClient() {
return () => { aliveRef.current = false }
}, [])
+ // genRef guards loadAll against stale-closure wallet-switch races.
+ // `snapAddr !== address` inside an async function compares two copies of
+ // the same closure-captured value — it's always equal even when the wallet
+ // has changed, so the check is a no-op. genRef is a mutable ref that every
+ // closure can read, so `gen !== genRef.current` correctly detects staleness.
+ const genRef = useRef(0)
+ useEffect(() => { genRef.current++ }, [address])
+
+ // B41: TradesPageClient can be the entry point (direct navigation to /trades)
+ // without DashboardClient or BotConfigPanel ever running. In that case the
+ // Zustand store has isSubscribed=false (initial default), causing the
+ // "Bot not configured" banner to appear for valid subscribers.
+ // Fix: fetch /user/{wallet}/public here too — it's lightweight, unauthenticated,
+ // and idempotent with the same fetch DashboardClient does.
+ useEffect(() => {
+ if (!address || !isConnected) return
+ let cancelled = false
+ const snap = address
+ getUserPublic(snap.toLowerCase())
+ .then(u => {
+ if (cancelled || snap !== address) return
+ setSubscribed(u.active)
+ setHlApiKeySet(u.hl_api_key_set)
+ setBotReadiness(u.hl_api_key_set ? 'saved' : 'unknown')
+ setPaperMode(!!u.paper_mode)
+ })
+ .catch(() => {})
+ return () => { cancelled = true }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [address, isConnected])
+
// Load public posts always; load private trades only if we have a view
// envelope. `forcedEnv` lets the in-page Unlock button pass a freshly-minted
// one so the user never has to detour through the Settings page first.
async function loadAll(forcedEnv?: SignedEnvelope) {
if (!address || !isConnected) {
- setTrades([]); setPosts([]); setLoading(false); setLoadErr(''); setNeedsUnlock(false)
+ setTrades([]); setLoading(false); setLoadErr(''); setNeedsUnlock(false)
return
}
+ const gen = genRef.current
+ const snapAddr = address // for API calls only — NOT for stale detection
setLoading(true)
const env = forcedEnv
- ?? getCachedViewEnvelope('view_trades', address)
- ?? getCachedViewEnvelope('view_user', address)
+ ?? getCachedViewEnvelope('view_trades', snapAddr)
+ ?? getCachedViewEnvelope('view_user', snapAddr)
let failed = false
try {
- const [t, p] = await Promise.all([
- env
- ? getTrades(address, env, 100, 1).catch(e => {
- failed = true
- setLoadErr(e instanceof Error ? e.message : 'Failed to load trades')
- return [] as BotTrade[]
- })
- : Promise.resolve([] as BotTrade[]),
- getPosts(500, 1).catch(() => [] as TrumpPost[]),
- ])
- if (!aliveRef.current) return
+ const t = env
+ ? await getTrades(snapAddr, env, 500, 1).catch(e => {
+ failed = true
+ setLoadErr(e instanceof Error ? e.message : 'Failed to load trades')
+ return [] as BotTrade[]
+ })
+ : []
+ // Use genRef — NOT `snapAddr !== address` (stale closure: both are the
+ // same closed-over value and the comparison is always false).
+ if (!aliveRef.current || gen !== genRef.current) return
setTrades(t)
- setPosts(p)
setNeedsUnlock(!env)
if (env && !failed) setLoadErr('')
} finally {
- if (aliveRef.current) setLoading(false)
+ if (aliveRef.current && gen === genRef.current) setLoading(false)
}
}
useEffect(() => {
+ // B27/B35: clear stale previous-wallet data BEFORE the async fetch so the
+ // UI never briefly shows another wallet's private trades.
+ setTrades([])
+ setNeedsUnlock(false)
+ setLoadErr('')
// On navigation we only use a cached envelope — never auto-popup the
// wallet. If none is cached the user unlocks explicitly via the button.
void loadAll()
@@ -84,8 +120,12 @@ export default function TradesPageClient() {
setUnlocking(true)
setLoadErr('')
try {
+ // view_user is a superset accepted by /trades, /positions/open,
+ // /positions/today — one signature unlocks everything on this page.
+ // Previously view_trades was used here, but that left OpenPositions
+ // locked (it requires view_positions or view_user).
const env = await getOrCreateViewEnvelope({
- action: 'view_trades', wallet: address, signMessageAsync,
+ action: 'view_user', wallet: address, signMessageAsync,
})
await loadAll(env)
} catch (e) {
@@ -97,7 +137,14 @@ export default function TradesPageClient() {
}
}
- const needsSetup = mounted && isConnected && (!isSubscribed || !hlApiKeySet)
+ // B40: paper users have no HL key but the bot IS configured for them — don't
+ // show "Bot not configured" just because hlApiKeySet is false.
+ // B41: on cold start (navigating directly to /trades), isSubscribed starts
+ // false in the store until DashboardClient or BotConfigPanel fetches
+ // /user/public. Guard with !loading so the banner only appears once
+ // the page has confirmed the wallet is genuinely unconfigured.
+ const needsSetup = mounted && isConnected && !loading &&
+ (!isSubscribed || (!hlApiKeySet && !paperMode))
return (
@@ -105,8 +152,7 @@ export default function TradesPageClient() {
{isZh ? '交易执行' : 'Trades'}
- What the bot actually did with your money — currently-open positions
- on top, closed-trade history with realized P&L below.
+ Open positions above · closed trade history with realized P&L below.
@@ -163,7 +209,7 @@ export default function TradesPageClient() {
)}
-
+
)
}
diff --git a/app/[locale]/trades/page.tsx b/app/[locale]/trades/page.tsx
index 22d5b4a..588ab23 100644
--- a/app/[locale]/trades/page.tsx
+++ b/app/[locale]/trades/page.tsx
@@ -34,10 +34,11 @@ const tradesDataset = {
'@id': `${siteUrl}/en/trades#dataset`,
name: 'Trump Alpha trade execution history',
description:
- 'Record of every signal-triggered trade: asset, direction, entry and exit price, realised P&L, hold time, and the source signal that triggered it. Public and timestamped.',
+ 'Per-wallet record of signal-triggered trades: asset, direction, entry and exit price, realised P&L, hold time, and the triggering signal. Private to each wallet owner — wallet signature required to view.',
url: `${siteUrl}/en/trades`,
- keywords: ['crypto trading track record', 'Hyperliquid', 'P&L', 'backtest', 'signal performance'],
- isAccessibleForFree: true,
+ keywords: ['crypto trading track record', 'Hyperliquid', 'P&L', 'signal performance'],
+ // Data is private (wallet-signed access), not freely accessible to the public
+ isAccessibleForFree: false,
creator: { '@type': 'Organization', name: 'Endorphin', url: siteUrl },
publisher: { '@id': `${siteUrl}/#org` },
license: `${siteUrl}/en/terms`,
diff --git a/app/[locale]/trump/TrumpPageClient.tsx b/app/[locale]/trump/TrumpPageClient.tsx
index f3b396d..e5dde32 100644
--- a/app/[locale]/trump/TrumpPageClient.tsx
+++ b/app/[locale]/trump/TrumpPageClient.tsx
@@ -1,10 +1,9 @@
'use client'
-import { useState, useEffect, useMemo } from 'react'
-import { useLocale } from 'next-intl'
+import { useState, useEffect, useMemo, useRef } from 'react'
import type { TrumpPost } from '@/types'
-import { getPosts } from '@/lib/api'
-import { swrFetch } from '@/lib/cache'
+import { getPosts, getPostsPage, type PostListResponse } from '@/lib/api'
+import { hasCached, swrFetch } from '@/lib/cache'
import PostRow, { isAiScored } from '@/components/dashboard/PostCards'
import SystemControl from '@/components/signals/SystemControl'
import PageHint from '@/components/ui/PageHint'
@@ -18,62 +17,147 @@ type SentimentFilter = (typeof SENTIMENTS)[number]
type SignalFilter = 'all' | 'actionable' | 'buy' | 'short'
interface TrumpSignalPageProps {
- initialPosts?: TrumpPost[] | null
+ initialData?: PostListResponse | null
}
-export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPageProps) {
- const locale = useLocale()
- const isZh = false
- const [posts, setPosts] = useState
(initialPosts ?? [])
- const [loading, setLoading] = useState(initialPosts === null)
+const EMPTY_COUNTS: PostListResponse['counts'] = {
+ all: 0,
+ actionable: 0,
+ buy: 0,
+ short: 0,
+ off_topic: 0,
+}
+
+function matchesBaseFilters(post: TrumpPost, sentFilter: SentimentFilter, hideNoise: boolean) {
+ if (hideNoise && !isAiScored(post)) return false
+ if (sentFilter !== 'all' && post.sentiment !== sentFilter) return false
+ return true
+}
+
+function matchesSignalFilter(post: TrumpPost, sigFilter: SignalFilter) {
+ if (sigFilter === 'actionable') return post.signal === 'buy' || post.signal === 'short'
+ if (sigFilter === 'buy') return post.signal === 'buy'
+ if (sigFilter === 'short') return post.signal === 'short'
+ return true
+}
+
+function buildLocalCounts(
+ posts: TrumpPost[],
+ sentFilter: SentimentFilter,
+ hideNoise: boolean,
+): PostListResponse['counts'] {
+ const sentimentScoped = posts.filter(p => sentFilter === 'all' || p.sentiment === sentFilter)
+ const base = sentimentScoped.filter(p => matchesBaseFilters(p, sentFilter, hideNoise))
+
+ return {
+ all: base.length,
+ actionable: base.filter(p => p.signal === 'buy' || p.signal === 'short').length,
+ buy: base.filter(p => p.signal === 'buy').length,
+ short: base.filter(p => p.signal === 'short').length,
+ off_topic: sentimentScoped.filter(p => !isAiScored(p)).length,
+ }
+}
+
+export default function TrumpSignalPage({ initialData = null }: TrumpSignalPageProps) {
+ const [posts, setPosts] = useState(initialData?.items ?? [])
+ const [totalPosts, setTotalPosts] = useState(initialData?.total ?? 0)
+ const [counts, setCounts] = useState(initialData?.counts ?? EMPTY_COUNTS)
+ const [loading, setLoading] = useState(initialData === null)
const [loadErr, setLoadErr] = useState('')
const [sentFilter, setSentFilter] = useState('all')
const [sigFilter, setSigFilter] = useState('all')
const [hideNoise, setHideNoise] = useState(false)
const [page, setPage] = useState(1)
+ const [serverPaging, setServerPaging] = useState(true)
+ // Tracks whether we already have any rows on screen, WITHOUT putting
+ // posts.length in the effect deps (which would re-run the effect on every
+ // setPosts and fire a redundant fetch). Updated after each load below.
+ const hasRowsRef = useRef((initialData?.items?.length ?? 0) > 0)
useEffect(() => {
+ const filters = {
+ // Don't apply the sentiment bias when a directional signal filter is
+ // active — Buy/Short/Actionable already imply direction, and combining
+ // them with a sentiment filter produces confusing empty results.
+ // sentFilter is intentionally NOT reset when the user switches tabs so
+ // their preference is preserved if they switch back to 'all'.
+ sentiment: sentFilter === 'all' || sigFilter !== 'all' ? undefined : sentFilter,
+ signal: sigFilter === 'all' ? undefined : sigFilter,
+ aiScoredOnly: hideNoise,
+ } as const
+ // sentFilter only participates in the key when sigFilter is 'all' — it's
+ // ignored by the query otherwise, so caching it in the key would create
+ // phantom cache entries that never match on the way back.
+ const effectiveSent = sigFilter === 'all' ? sentFilter : 'all'
+ const key = `posts-truth-page-${page}-sent-${effectiveSent}-sig-${sigFilter}-noise-${hideNoise ? '1' : '0'}`
+ const legacyKey = 'posts-truth-legacy-500'
+ setLoadErr('')
+ setLoading(!hasRowsRef.current && !hasCached(key) && !hasCached(legacyKey))
+
swrFetch(
- 'posts-500',
+ key,
3 * 60_000,
- () => getPosts(500, 1),
- fresh => setPosts(fresh),
+ () => getPostsPage(PAGE_SIZE, page, 'truth', filters),
+ fresh => {
+ setPosts(fresh.items)
+ setTotalPosts(fresh.total)
+ setCounts(fresh.counts)
+ hasRowsRef.current = fresh.items.length > 0
+ },
)
- .then(p => { setPosts(p); setLoadErr('') })
- .catch(e => setLoadErr(e instanceof Error ? e.message : 'Failed to load posts'))
+ .then(r => {
+ setPosts(r.items)
+ setTotalPosts(r.total)
+ setCounts(r.counts)
+ setServerPaging(true)
+ setLoadErr('')
+ hasRowsRef.current = r.items.length > 0
+ })
+ .catch(async e => {
+ const detail = e instanceof Error ? e.message : 'Failed to load posts'
+ if (!detail.includes('404')) {
+ setLoadErr(detail)
+ return
+ }
+
+ try {
+ // Legacy fallback (old backend without /posts-paged). Pull the
+ // largest window the /posts endpoint allows (le=500) so client-side
+ // pagination below has the full set to slice — 200 silently dropped
+ // older matching posts once the truth feed grew past one page.
+ const legacyPosts = await swrFetch(
+ legacyKey,
+ 3 * 60_000,
+ () => getPosts(500, 1, 'truth'),
+ )
+ const localCounts = buildLocalCounts(legacyPosts, sentFilter, hideNoise)
+ setPosts(legacyPosts)
+ setTotalPosts(localCounts.all)
+ setCounts(localCounts)
+ setServerPaging(false)
+ setLoadErr('')
+ hasRowsRef.current = legacyPosts.length > 0
+ } catch (legacyErr) {
+ setLoadErr(legacyErr instanceof Error ? legacyErr.message : detail)
+ }
+ })
.finally(() => setLoading(false))
- }, [])
+ }, [page, sentFilter, sigFilter, hideNoise])
- const trumpPosts = useMemo(
- () => posts.filter(p => (p.source || '') === 'truth'),
- [posts],
+ const filtered = useMemo(
+ () => serverPaging
+ ? posts
+ : posts.filter(p => matchesBaseFilters(p, sentFilter, hideNoise) && matchesSignalFilter(p, sigFilter)),
+ [hideNoise, posts, sentFilter, serverPaging, sigFilter],
)
- const noiseCount = useMemo(
- () => trumpPosts.filter(p => !isAiScored(p)).length,
- [trumpPosts],
- )
-
- const filtered = useMemo(() => trumpPosts.filter(p => {
- if (hideNoise && !isAiScored(p)) return false
- if (sentFilter !== 'all' && p.sentiment !== sentFilter) return false
- if (sigFilter === 'actionable' && p.signal !== 'buy' && p.signal !== 'short') return false
- if (sigFilter === 'buy' && p.signal !== 'buy') return false
- if (sigFilter === 'short' && p.signal !== 'short') return false
- return true
- }), [trumpPosts, sentFilter, sigFilter, hideNoise])
-
- const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
+ const noiseCount = counts.off_topic
+ const totalPages = Math.max(1, Math.ceil(totalPosts / PAGE_SIZE))
const safePage = Math.min(page, totalPages)
- const pageItems = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE)
-
- const sigCounts = useMemo(() => ({
- all: trumpPosts.length,
- actionable: trumpPosts.filter(p => p.signal === 'buy' || p.signal === 'short').length,
- buy: trumpPosts.filter(p => p.signal === 'buy').length,
- short: trumpPosts.filter(p => p.signal === 'short').length,
- }), [trumpPosts])
+ const pageItems = serverPaging
+ ? posts
+ : filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE)
const signalLabels: Record = {
all: 'All', actionable: 'Actionable', buy: 'Buy', short: 'Short',
@@ -86,10 +170,25 @@ export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPage
-
① Trump Signal
-
- Watches Trump's Truth Social posts in real time, AI-scores each one,
- and only fires a trade when conviction is high enough to move the market.
+
+
Trump Signal
+
+ ⚡ Auto-trade
+
+
+
+ Every Truth Social post scored in <3s. Trades only when conviction clears the threshold.
Live
@@ -112,56 +211,74 @@ export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPage
{ setSigFilter(f.key); setPage(1) }}
+ onClick={() => {
+ setSigFilter(f.key)
+ setPage(1)
+ // sentFilter is intentionally NOT reset here — the user's
+ // bias preference is preserved so it's still active when
+ // they switch back to 'all'. The query layer ignores
+ // sentFilter whenever sigFilter is directional.
+ }}
>
{f.key === 'actionable' ? '🔥 ' : ''}
- {signalLabels[f.key]}
- {sigCounts[f.key]}
+ {signalLabels[f.key]}{' '}
+ {counts[f.key]}
))}
- {/* Sentiment filter */}
-
- {/* One-click collapse of off-topic (non-crypto, un-scored) posts. */}
- {noiseCount > 0 && (
- { setHideNoise(v => !v); setPage(1) }}
- title={`Show only crypto-relevant posts — hides ${noiseCount} off-topic Trump posts`}
- style={{
- padding: '4px 10px', borderRadius: 6,
- border: '1px solid var(--line)',
- background: hideNoise ? 'var(--ink)' : 'transparent',
- color: hideNoise ? 'var(--bg)' : 'var(--ink-3)',
- fontSize: 11, cursor: 'pointer', fontWeight: 600,
- marginRight: 4,
- }}
- >
- {hideNoise ? 'Show all' : 'Signals only'}
-
- )}
-
- Bias
-
-
- {SENTIMENTS.map(f => (
- { setSentFilter(f); setPage(1) }}
- style={{
- padding: '4px 10px', borderRadius: 6, border: '1px solid var(--line)',
- background: sentFilter === f ? 'var(--ink)' : 'transparent',
- color: sentFilter === f ? 'var(--bg)' : 'var(--ink-3)',
- fontSize: 11, cursor: 'pointer', fontWeight: sentFilter === f ? 600 : 400,
- }}
- >
- {sentimentLabels[f]}
-
- ))}
-
+ {/* Sentiment + noise filters — hidden when a directional filter is
+ already active (actionable / buy / short), because:
+ - "buy" already implies bullish direction
+ - "short" already implies bearish direction
+ - "actionable" = buy ∪ short — sentiment adds no further info
+ Showing them would create confusing combinations (e.g. Buy + Bearish
+ returns 0 results with no explanation). */}
+ {sigFilter === 'all' && (
+
+ {/* One-click collapse of off-topic (non-crypto, un-scored) posts.
+ Hidden when a sentiment filter is active — Bullish/Bearish/Neutral
+ results are all AI-scored by definition, so there are no off-topic
+ posts to hide and the button would be meaningless. */}
+ {(noiseCount > 0 || hideNoise) && sentFilter === 'all' && (
+ { setHideNoise(v => !v); setPage(1) }}
+ title={`Show only crypto-relevant posts — hides ${noiseCount} off-topic Trump posts`}
+ style={{
+ padding: '4px 10px', borderRadius: 6,
+ border: '1px solid var(--line)',
+ background: hideNoise ? 'var(--ink)' : 'transparent',
+ color: hideNoise ? 'var(--bg)' : 'var(--ink-3)',
+ fontSize: 11, cursor: 'pointer', fontWeight: 600,
+ marginRight: 4,
+ }}
+ >
+ {hideNoise ? 'Show all' : 'Signals only'}
+
+ )}
+
+ Bias
+
+
+ {SENTIMENTS.map(f => (
+ { setSentFilter(f); setPage(1) }}
+ style={{
+ padding: '4px 10px', borderRadius: 6, border: '1px solid var(--line)',
+ background: sentFilter === f ? 'var(--ink)' : 'transparent',
+ color: sentFilter === f ? 'var(--bg)' : 'var(--ink-3)',
+ fontSize: 11, cursor: 'pointer', fontWeight: sentFilter === f ? 600 : 400,
+ }}
+ >
+ {sentimentLabels[f]}
+
+ ))}
+
+ )}
{loading && }
@@ -176,7 +293,7 @@ export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPage
)}
- {!loading && !loadErr && filtered.length === 0 && (
+ {!loading && !loadErr && totalPosts === 0 && (
No Trump signals match the current filter.
@@ -191,7 +308,7 @@ export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPage
diff --git a/app/[locale]/trump/page.tsx b/app/[locale]/trump/page.tsx
index a5e8cc6..65e0e95 100644
--- a/app/[locale]/trump/page.tsx
+++ b/app/[locale]/trump/page.tsx
@@ -1,7 +1,8 @@
-import { getPosts } from '@/lib/api'
+import { type PostListResponse } from '@/lib/api'
import type { Metadata } from 'next'
import TrumpPageClient from './TrumpPageClient'
import Breadcrumbs from '@/components/seo/Breadcrumbs'
+import { getInitialPostPage } from '@/lib/postPage'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://trumpsignal.com'
export const revalidate = 30
@@ -75,7 +76,10 @@ const trumpDataset = {
}
export default async function TrumpPage() {
- const posts = await getPosts(500, 1).catch(() => null)
+ const initialData: PostListResponse | null = await getInitialPostPage(30, 1, {
+ source: 'truth',
+ legacyFallbackSource: 'truth',
+ })
return (
<>
@@ -84,7 +88,7 @@ export default async function TrumpPage() {
dangerouslySetInnerHTML={{ __html: JSON.stringify(trumpDataset) }}
/>
-
+
>
)
}
diff --git a/app/landing.css b/app/landing.css
index 849b3ef..ec2ef22 100644
--- a/app/landing.css
+++ b/app/landing.css
@@ -127,17 +127,20 @@
}
.lp-nav-cta {
display: inline-flex; align-items: center; gap: 8px;
- padding: 9px 16px;
- font-size: 13px; font-weight: 600;
+ padding: 9px 18px;
+ font-size: 13px; font-weight: 700;
color: #0a0907;
- background: var(--lp-amber);
+ background: linear-gradient(135deg, #ffd88a, var(--lp-amber) 55%, #ff9a4d);
border-radius: 999px;
text-decoration: none;
transition: transform 0.15s ease, box-shadow 0.15s ease;
+ box-shadow: 0 4px 16px var(--lp-amber-glow), inset 0 1px 0 rgba(255,255,255,0.3);
+ letter-spacing: -0.01em;
}
.lp-nav-cta:hover {
transform: translateY(-1px);
- box-shadow: 0 10px 30px var(--lp-amber-glow);
+ box-shadow: 0 10px 30px var(--lp-amber-glow), inset 0 1px 0 rgba(255,255,255,0.3);
+ filter: brightness(1.06);
}
/* ---------- Hero ---------- */
@@ -175,15 +178,16 @@
.lp-live-badge {
display: inline-flex; align-items: center; gap: 8px;
margin: 0 0 22px;
- padding: 5px 12px 5px 9px;
+ padding: 6px 14px 6px 10px;
font-size: 11px; font-weight: 700;
- color: var(--lp-ink-3);
- background: rgba(255, 255, 255, 0.04);
- border: 1px solid rgba(255, 255, 255, 0.10);
- border-radius: 6px;
- letter-spacing: 0.08em;
+ color: var(--lp-ink-2);
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 8px;
+ letter-spacing: 0.09em;
text-transform: uppercase;
animation: lp-fade-up 0.7s ease both;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
}
.lp-live-dot {
width: 8px; height: 8px; border-radius: 50%;
@@ -338,16 +342,17 @@
.lp-eyebrow {
font-size: 11px;
font-weight: 700;
- letter-spacing: 0.14em;
+ letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--lp-amber);
margin-bottom: 12px;
+ text-shadow: 0 0 20px rgba(245,165,36,0.35);
}
.lp-h2 {
- font-size: clamp(26px, 3.8vw, 48px);
+ font-size: clamp(26px, 3.8vw, 52px);
font-weight: 800;
- letter-spacing: -0.03em;
- line-height: 1.1;
+ letter-spacing: -0.035em;
+ line-height: 1.08;
margin: 0 0 16px;
}
.lp-lead {
@@ -410,9 +415,10 @@
.lp-step-num {
font-family: 'Geist Mono', monospace;
font-size: 12px;
- color: var(--lp-ink-4);
+ color: var(--lp-amber);
letter-spacing: 0.1em;
margin-bottom: 20px;
+ opacity: 0.7;
}
.lp-step-icon {
width: 52px; height: 52px;
@@ -425,13 +431,13 @@
}
.lp-step h3 {
font-size: 20px;
- font-weight: 600;
+ font-weight: 700;
margin: 0 0 10px;
- letter-spacing: -0.01em;
+ letter-spacing: -0.02em;
}
.lp-step p {
font-size: 15px;
- line-height: 1.6;
+ line-height: 1.62;
color: var(--lp-ink-3);
margin: 0;
}
@@ -450,30 +456,47 @@
background: var(--lp-surface);
border: 1px solid var(--lp-line);
border-radius: 16px;
- transition: all 0.2s ease;
+ transition: transform 0.22s ease, border-color 0.22s ease, background 0.22s ease;
+ position: relative;
+ overflow: hidden;
+}
+.lp-feat::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ padding: 1px;
+ background: linear-gradient(135deg, rgba(245,165,36,0.4), transparent 55%);
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ opacity: 0;
+ transition: opacity 0.28s ease;
}
.lp-feat:hover {
background: var(--lp-surface-2);
- border-color: var(--lp-line-2);
- transform: translateY(-2px);
+ border-color: rgba(245,165,36,0.22);
+ transform: translateY(-3px);
}
+.lp-feat:hover::before { opacity: 1; }
.lp-feat-icon {
- width: 36px; height: 36px;
- border-radius: 10px;
+ width: 40px; height: 40px;
+ border-radius: 11px;
display: grid; place-items: center;
margin-bottom: 16px;
- background: rgba(245,165,36,0.1);
+ background: linear-gradient(135deg, rgba(245,165,36,0.18), rgba(245,165,36,0.06));
+ border: 1px solid rgba(245,165,36,0.18);
color: var(--lp-amber);
}
.lp-feat h4 {
font-size: 16px;
- font-weight: 600;
+ font-weight: 700;
margin: 0 0 8px;
- letter-spacing: -0.01em;
+ letter-spacing: -0.015em;
}
.lp-feat p {
font-size: 14px;
- line-height: 1.55;
+ line-height: 1.58;
color: var(--lp-ink-3);
margin: 0;
}
@@ -575,7 +598,7 @@
/* ---------- Final CTA ---------- */
.lp-cta-final {
- padding: 88px 0 96px;
+ padding: 96px 0 104px;
text-align: center;
position: relative;
}
@@ -583,10 +606,10 @@
content: '';
position: absolute;
left: 50%; top: 50%;
- width: 700px; height: 700px;
- background: radial-gradient(circle, rgba(245,165,36,0.18), transparent 60%);
+ width: 800px; height: 700px;
+ background: radial-gradient(ellipse, rgba(245,165,36,0.22) 0%, rgba(239,68,68,0.10) 45%, transparent 70%);
transform: translate(-50%, -50%);
- filter: blur(80px);
+ filter: blur(90px);
pointer-events: none;
z-index: -1;
}
@@ -646,23 +669,25 @@
}
.lp-stat-val {
font-family: 'Geist', sans-serif;
- font-size: 34px;
- font-weight: 700;
- letter-spacing: -0.02em;
+ font-size: 38px;
+ font-weight: 800;
+ letter-spacing: -0.03em;
line-height: 1;
color: var(--lp-ink);
margin-bottom: 8px;
}
.lp-stat-val .suffix {
- font-size: 18px;
+ font-size: 20px;
color: var(--lp-amber);
- margin-left: 2px;
+ margin-left: 3px;
+ font-weight: 700;
}
.lp-stat-lbl {
- font-size: 12px;
+ font-size: 11px;
color: var(--lp-ink-3);
text-transform: uppercase;
- letter-spacing: 0.08em;
+ letter-spacing: 0.1em;
+ font-weight: 600;
}
.lp-stat-sub {
font-size: 11px;
@@ -681,17 +706,18 @@
.lp-post-card {
padding: 24px;
- background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01));
+ background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.01));
border: 1px solid var(--lp-line);
border-radius: 16px;
display: flex;
gap: 18px;
align-items: flex-start;
- transition: all 0.25s ease;
+ transition: transform 0.22s ease, border-color 0.22s ease;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
.lp-post-card:hover {
- border-color: var(--lp-line-2);
- transform: translateY(-2px);
+ border-color: rgba(245,165,36,0.2);
+ transform: translateY(-3px);
}
.lp-post-kind {
width: 44px; height: 44px; flex-shrink: 0;
@@ -891,11 +917,12 @@
grid-template-columns: repeat(5, 1fr);
gap: 0;
margin-top: 48px;
- padding: 32px 16px;
- background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.005));
+ padding: 36px 20px;
+ background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01));
border: 1px solid var(--lp-line);
border-radius: 20px;
position: relative;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
@media (max-width: 880px) { .lp-flow { grid-template-columns: 1fr; gap: 20px; } }
@@ -907,11 +934,12 @@
.lp-flow-step:not(:last-child)::after {
content: '→';
position: absolute;
- right: -8px;
- top: 28px;
- font-size: 20px;
+ right: -10px;
+ top: 26px;
+ font-size: 22px;
color: var(--lp-amber);
- opacity: 0.6;
+ opacity: 0.7;
+ text-shadow: 0 0 14px rgba(245,165,36,0.5);
}
@media (max-width: 880px) {
.lp-flow-step:not(:last-child)::after { display: none; }
@@ -919,15 +947,16 @@
.lp-flow-badge {
display: inline-flex; align-items: center; justify-content: center;
- width: 56px; height: 56px;
+ width: 58px; height: 58px;
border-radius: 16px;
- background: linear-gradient(135deg, rgba(245,165,36,0.2), rgba(245,165,36,0.05));
- border: 1px solid rgba(245,165,36,0.3);
+ background: linear-gradient(135deg, rgba(245,165,36,0.22), rgba(245,165,36,0.06));
+ border: 1px solid rgba(245,165,36,0.35);
color: var(--lp-amber);
- font-weight: 700; font-size: 18px;
+ font-weight: 800; font-size: 18px;
font-family: 'Geist Mono', monospace;
margin-bottom: 14px;
position: relative;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
}
.lp-flow-badge::before {
content: '';
@@ -938,10 +967,11 @@
z-index: -1;
}
.lp-flow-step h5 {
- font-size: 14px; font-weight: 600; margin: 0 0 6px;
+ font-size: 14px; font-weight: 700; margin: 0 0 6px;
+ letter-spacing: -0.01em;
}
.lp-flow-step p {
- font-size: 12px; line-height: 1.5;
+ font-size: 12px; line-height: 1.55;
color: var(--lp-ink-3); margin: 0;
}
.lp-flow-time {
@@ -969,11 +999,12 @@
background: var(--lp-surface);
border: 1px solid var(--lp-line);
border-radius: 14px;
- transition: all 0.2s ease;
+ transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.lp-faq-item:hover {
background: var(--lp-surface-2);
- border-color: var(--lp-line-2);
+ border-color: rgba(245,165,36,0.2);
+ transform: translateY(-2px);
}
.lp-faq-q {
display: flex; align-items: flex-start; gap: 10px;
@@ -1686,12 +1717,18 @@
animation: lp-fade-up 0.9s 0.55s ease both;
}
.lp-engine-chip {
- font-size: 11px; font-weight: 600;
- padding: 4px 10px; border-radius: 5px;
+ font-size: 11px; font-weight: 700;
+ padding: 5px 11px; border-radius: 6px;
font-family: 'Geist Mono', monospace;
- letter-spacing: 0.04em;
+ letter-spacing: 0.05em;
border: 1px solid;
white-space: nowrap;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
+ transition: opacity 0.15s ease, transform 0.15s ease;
+}
+.lp-engine-chip:hover {
+ opacity: 0.85;
+ transform: translateY(-1px);
}
.lp-engine-chip.trump { color: var(--lp-amber); background: rgba(245,165,36,0.10); border-color: rgba(245,165,36,0.28); }
.lp-engine-chip.macro { color: var(--lp-green); background: rgba(31,219,99,0.09); border-color: rgba(31,219,99,0.25); }
@@ -1715,12 +1752,12 @@
}
.lp-livefeed-head {
display: flex; align-items: center; gap: 8px;
- padding: 11px 16px;
+ padding: 12px 16px;
border-bottom: 1px solid rgba(255,255,255,0.07);
font-size: 10px; font-weight: 700;
- letter-spacing: 0.12em; text-transform: uppercase;
- color: var(--lp-ink-3);
- background: rgba(255,255,255,0.015);
+ letter-spacing: 0.13em; text-transform: uppercase;
+ color: var(--lp-ink-2);
+ background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
}
.lp-livefeed-dot {
width: 7px; height: 7px; border-radius: 50%;
@@ -1968,10 +2005,11 @@
padding-top: 12px; border-top: 1px solid var(--lp-line);
}
.lp-pillar-metric {
- font-size: 22px; font-weight: 700;
+ font-size: 26px; font-weight: 800;
color: var(--lp-amber);
font-family: 'Geist Mono', ui-monospace, monospace;
line-height: 1;
+ letter-spacing: -0.02em;
}
.lp-pillar-metric-lbl {
font-size: 11px; color: var(--lp-ink-4);
@@ -1985,10 +2023,21 @@
/* reduce motion respect */
@media (prefers-reduced-motion: reduce) {
+ /* Catch-all: neutralise all 14 infinite animations (lp-scroll marquee,
+ lp-float blobs, lp-ping/lp-pulse dots, lp-blink cursor, …) plus every
+ entrance animation. 0.01ms keeps onAnimationEnd handlers firing. */
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
.lp-noise,
.lp-pillar-edge { animation: none !important; }
.lp-pillar { transform: none !important; transition: none; }
.lp-spotlight { display: none; }
+ /* Marquee: park at origin instead of mid-translate(-50%). */
+ .lp-ticker-track { animation: none !important; transform: none !important; }
}
/* ── FAQ ──────────────────────────────────────────────── */
@@ -2006,7 +2055,9 @@
background: var(--lp-surface);
transition: border-color 0.2s;
}
-.lp-faq-item:hover { border-color: var(--lp-line-2); }
+.lp-faq-item:hover {
+ border-color: rgba(245,165,36,0.28);
+}
.lp-faq-q {
width: 100%;
display: flex;
diff --git a/app/layout.tsx b/app/layout.tsx
index d8aa8c4..4ece59a 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -5,7 +5,7 @@ import './[locale]/globals.css'
const siteTitle = 'Trump Alpha'
const siteTagline = 'The crypto signals that move price before the crowd'
-const siteDescription = "Endorphin is a crypto research desk tracking four signals that move markets before consensus catches up — Trump's posts, BTC macro bottoms, funding-rate extremes, and what KOLs do versus what they say. Every signal is public and timestamped."
+const siteDescription = "Endorphin is a crypto research desk tracking six signals that move markets before consensus catches up — Trump's posts, BTC macro bottoms, funding-rate extremes, KOL long-form calls, talks-vs-trades divergence, and the Breakout Monitor. Every signal is public and timestamped."
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL
export const metadata: Metadata = {
@@ -283,7 +283,7 @@ const jsonLd = {
name: 'What is Trump Alpha?',
acceptedAnswer: {
'@type': 'Answer',
- text: 'Trump Alpha is an AI-powered crypto intelligence dashboard that monitors four signal sources: Trump Truth Social posts, BTC macro-bottom confluence signals, KOL Substack and podcast essays, and talks-vs-trades divergence between public commentary and on-chain behavior.',
+ text: 'Trump Alpha is an AI-powered crypto intelligence dashboard that monitors six signal sources: Trump Truth Social posts, BTC macro-bottom confluence signals, KOL Substack and podcast essays, talks-vs-trades divergence between public commentary and on-chain behavior, a funding-rate reversal trigger, and a real-time breakout monitor for ETH and LINK.',
},
},
{
@@ -315,7 +315,7 @@ const jsonLd = {
name: 'Which KOLs does Trump Alpha track?',
acceptedAnswer: {
'@type': 'Answer',
- text: 'Trump Alpha tracks 19 KOL feeds including Arthur Hayes (BitMEX), Delphi Digital, Dragonfly Capital, Pomp, and major crypto podcasts (Empire, Bankless, Unchained, 0xResearch, Lightspeed). Posts and episodes are processed daily; AI extracts ticker calls, direction (bullish/bearish/buy/sell), and conviction scores.',
+ text: 'Trump Alpha tracks 25 KOL feeds including Arthur Hayes (BitMEX), Delphi Digital, Pomp, and major crypto podcasts (Empire, Bankless, Unchained, 0xResearch, Lightspeed). Posts and episodes are processed daily; AI extracts ticker calls, direction (bullish/bearish/buy/sell), and conviction scores.',
},
},
{
@@ -323,7 +323,7 @@ const jsonLd = {
name: 'Is Trump Alpha free?',
acceptedAnswer: {
'@type': 'Answer',
- text: 'Yes. All four signal dashboards (Trump, Macro Vibes, KOL, talks-vs-trades) are free to read. The optional Hyperliquid auto-trader requires your own Hyperliquid account and API key. Trump Alpha never takes custody of your funds — your API key can open and close positions but cannot withdraw.',
+ text: 'Yes. All six signal dashboards are free to read. The optional Hyperliquid auto-trader requires a subscription, your own Hyperliquid account, and an API key. Trump Alpha never takes custody of your funds — your API key can open and close positions but cannot withdraw.',
},
},
{
@@ -380,7 +380,7 @@ const jsonLd = {
name: 'What crypto signals do KOL newsletters give?',
acceptedAnswer: {
'@type': 'Answer',
- text: 'Trump Alpha ingests 19 crypto KOL newsletters, blogs, and podcast feeds daily — including Arthur Hayes, Delphi Digital, Dragonfly Capital, Bankless, and Unchained. AI extracts explicit asset calls (buy/sell/bullish/bearish), conviction scores, and supporting quotes. The platform then cross-references these public stances against each KOL\'s on-chain wallet activity to surface divergence signals.',
+ text: 'Trump Alpha ingests 25 crypto KOL newsletters, blogs, and podcast feeds daily — including Arthur Hayes, Delphi Digital, Bankless, and Unchained. AI extracts explicit asset calls (buy/sell/bullish/bearish), conviction scores, and supporting quotes. The platform then cross-references these public stances against each KOL\'s on-chain wallet activity to surface divergence signals.',
},
},
{
@@ -429,7 +429,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
}
return (
-
+