Files
signal-platform/frontend/src/pages/DesignHorizonPage.tsx
T
dennisthiessenandClaude Fable 5 20f6981712
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m9s
Deploy / deploy (push) Successful in 36s
Horizon redesign: space theme, visual dashboard, chart reskin
Replaces the citron/green glass theme with the Horizon direction
(mockup at /design-horizon): space-void background, starfield, dim
Mars horizon, rim-cyan accent, rose for negative. Palette validated
for CVD separation and contrast on the dark surface.

- tailwind.config: gray -> cool space neutrals, blue/emerald -> cyan
  scale, red -> rose; display font Space Grotesk
- globals.css: Horizon tokens, atmosphere, denser glass, cyan
  buttons/inputs, price-rail / R-bar / radar chart CSS
- Dashboard rebuilt: verdict hero, top-pick card with spatial price
  rail, KPI tiles, open positions as diverging R-bars with expandable
  detail + trade chart (OHLCV since entry, entry/stop/target levels),
  radar list with per-setup disqualify reason, watchlist chips
- OpenTradesPanel: table replaced by R-bar rows; all fields kept in
  the drill-down (shares, P&L, alpha, trailing stop, sell)
- qualification: disqualifyReason() mirrors qualifiesSetup rule order
- CandlestickChart: canvas colors moved to Horizon palette; S/R now
  cyan/neutral so rose stays reserved for the stop level
- ScoreCard: radar score fingerprint with hover values
- Design mockup pages included (routed in App.tsx)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 07:24:17 +02:00

1392 lines
60 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Design study "Horizon" — v2 mockup.
*
* Fully self-contained: static mock data, no hooks into the API, no auth.
* View at /design-horizon with `npm run dev` — no backend or DB needed.
*
* Direction: space void + dim Mars horizon (glow stays atmosphere, never data),
* one focal "what should I do today" card, zero classic tables, drill-down
* everywhere. Chart colors validated (CVD + contrast) against the dark surface.
*/
import { useMemo, useRef, useState } from 'react';
/* ------------------------------------------------------------------ */
/* Mock data */
/* ------------------------------------------------------------------ */
const heroSetup = {
symbol: 'VRT',
name: 'Vertiv Holdings',
direction: 'long' as const,
entry: 92.1,
stop: 86.4,
current: 94.3,
target: 107.0,
rr: 2.6,
targetProb: 58,
residualPct: 94,
conviction: 'High',
sizeHint: 'vol-adjusted size 6.2% of book',
};
const positions = [
{
symbol: 'ANET', name: 'Arista Networks', direction: 'long', r: 1.4,
entry: 118.4, current: 131.2, shares: 25, opened: 'May 28',
stop: 121.8, stopNote: 'trailed from 111.10', target: 140.0,
alphaUsd: 310, exit: 'ATR trailing stop',
spark: [118.4, 119.2, 121.5, 120.8, 123.4, 125.1, 124.2, 126.8, 128.3, 127.5, 129.9, 131.2],
stopTrail: [111.1, 111.1, 111.1, 112.6, 114.2, 114.2, 115.8, 117.4, 117.4, 119.5, 121.8, 121.8],
},
{
symbol: 'VRT', name: 'Vertiv Holdings', direction: 'long', r: 1.1,
entry: 88.3, current: 94.3, shares: 40, opened: 'Jun 12',
stop: 86.9, stopNote: 'trailed from 83.10', target: 103.0,
alphaUsd: 205, exit: 'ATR trailing stop',
spark: [88.3, 89.1, 88.6, 90.2, 91.4, 90.8, 92.1, 93.0, 92.4, 93.6, 94.1, 94.3],
stopTrail: [83.1, 83.1, 84.0, 84.0, 84.9, 84.9, 85.6, 85.6, 86.2, 86.2, 86.9, 86.9],
},
{
symbol: 'CLS', name: 'Celestica', direction: 'long', r: 0.4,
entry: 61.2, current: 62.9, shares: 60, opened: 'Jun 30',
stop: 57.1, stopNote: 'initial stop', target: 71.5,
alphaUsd: 84, exit: 'ATR trailing stop',
spark: [61.2, 61.8, 61.1, 62.3, 62.0, 62.6, 63.1, 62.7, 62.4, 62.9, 63.2, 62.9],
stopTrail: [57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1],
},
{
symbol: 'SMCI', name: 'Super Micro Computer', direction: 'short', r: -0.6,
entry: 44.8, current: 46.1, shares: 80, opened: 'Jul 2',
stop: 47.0, stopNote: 'initial stop', target: 38.5,
alphaUsd: -57, exit: 'ATR trailing stop',
spark: [44.8, 44.2, 45.0, 45.6, 45.1, 45.9, 46.4, 45.8, 46.2, 46.0, 46.3, 46.1],
stopTrail: [47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0],
},
];
const radar = [
{
rank: 2, symbol: 'CLS', name: 'Celestica', direction: 'long',
residual: 91, rr: '2.2:1', prob: 61, qualified: true, note: 'clears EV gate',
},
{
rank: 3, symbol: 'APP', name: 'AppLovin', direction: 'long',
residual: 89, rr: '2.4:1', prob: 55, qualified: true, note: 'clears EV gate',
},
{
rank: 4, symbol: 'NVDA', name: 'NVIDIA', direction: 'long',
residual: 87, rr: '1.6:1', prob: 63, qualified: false, note: 'EV below gate — payoff too thin',
},
{
rank: 5, symbol: 'AMD', name: 'Advanced Micro Devices', direction: 'long',
residual: 84, rr: '2.1:1', prob: 48, qualified: false, note: 'extended — 12% above entry zone',
},
{
rank: 6, symbol: 'PLTR', name: 'Palantir', direction: 'short',
residual: 82, rr: '1.9:1', prob: 44, qualified: false, note: 'counter-trend in neutral regime',
},
];
const watchlist = [
{ symbol: 'VRT', pct: 3.1 }, { symbol: 'ANET', pct: 2.4 },
{ symbol: 'NVDA', pct: 1.8 }, { symbol: 'AVGO', pct: 1.5 },
{ symbol: 'CRWD', pct: 0.9 }, { symbol: 'MSFT', pct: 0.3 },
{ symbol: 'PLTR', pct: -0.9 }, { symbol: 'META', pct: -1.2 },
];
/* Weekly closes since January, indexed to 100 — one axis, common base. */
const perfPortfolio = [100, 101.2, 100.6, 102.3, 103.8, 103.1, 105.0, 106.4, 105.2, 107.9, 109.6, 108.8, 111.3, 110.1, 112.6, 114.9, 113.8, 116.2, 118.4, 117.1, 119.8, 121.6, 120.4, 123.1, 125.0, 126.4];
const perfSpy = [100, 100.8, 100.2, 101.5, 102.1, 101.4, 102.6, 103.3, 102.5, 103.9, 104.6, 103.8, 104.9, 104.1, 105.2, 106.0, 105.3, 106.4, 107.2, 106.5, 107.4, 108.1, 107.3, 108.0, 108.6, 108.2];
const perfStart = new Date(2026, 0, 9);
const perfDates = perfPortfolio.map((_, i) => {
const d = new Date(perfStart);
d.setDate(d.getDate() + i * 7);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
});
const alphaSpark = [0, 42, 30, 95, 150, 128, 210, 265, 231, 340, 415, 388, 470, 442, 508, 570, 538, 590, 642];
/* ~3 months of daily VRT candles, seeded so the mock is stable across reloads. */
type Candle = { d: Date; o: number; h: number; l: number; c: number; v: number };
const vrtCandles: Candle[] = (() => {
let s = 42 >>> 0;
const rnd = () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; };
const days: Date[] = [];
const cursor = new Date(2026, 6, 9);
while (days.length < 64) {
const dow = cursor.getDay();
if (dow !== 0 && dow !== 6) days.unshift(new Date(cursor));
cursor.setDate(cursor.getDate() - 1);
}
const drift = (i: number) => (i < 20 ? 0.30 : i < 28 ? -0.38 : i < 46 ? 0.44 : i < 53 ? -0.32 : 0.52);
const out: Candle[] = [];
let close = 73.6;
for (let i = 0; i < 64; i++) {
const chg = drift(i) + (rnd() - 0.5) * 2.4;
const o = close;
const c = o + chg;
out.push({
d: days[i], o, c,
h: Math.max(o, c) + rnd() * 1.2,
l: Math.min(o, c) - rnd() * 1.2,
v: 1.4e6 * (0.55 + rnd()) * (1 + Math.abs(chg) / 2.5),
});
close = c;
}
const shift = 94.3 - close;
return out.map((b) => ({ ...b, o: b.o + shift, h: b.h + shift, l: b.l + shift, c: b.c + shift }));
})();
const tickerLevels = {
resistance: 96.4,
supportLo: 88.7,
supportHi: 90.1,
};
/* Score fingerprint — all axes 0100 percentile, same order everywhere. */
const RADAR_AXES = ['Momentum', 'Trend', 'Setup', 'Sentiment', 'Fundam.'];
const RADAR_AXES_FULL = [
'Residual momentum', 'Trend strength', 'Setup quality', 'News sentiment', 'Fundamentals',
];
const heroScores = [94, 88, 85, 71, 62];
const liveMedian = [68, 61, 57, 52, 55];
const qualifiedCompare = [
{ symbol: 'VRT', tag: 'top pick', scores: heroScores },
{ symbol: 'CLS', tag: 'rank 2', scores: [91, 82, 72, 58, 74] },
{ symbol: 'APP', tag: 'rank 3', scores: [89, 78, 69, 66, 81] },
];
/* ------------------------------------------------------------------ */
/* Small building blocks */
/* ------------------------------------------------------------------ */
function fmt(n: number, digits = 2): string {
return n.toLocaleString('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits });
}
function signed(n: number, unit = ''): string {
return `${n > 0 ? '+' : n < 0 ? '' : ''}${fmt(Math.abs(n))}${unit}`;
}
function DirTag({ direction }: { direction: string }) {
return (
<span className={`hzn-tag ${direction === 'long' ? 'hzn-tag-long' : 'hzn-tag-short'}`}>
{direction}
</span>
);
}
/** 2px sparkline, endpoint dot with surface ring. Single series — no legend. */
function Spark({ data, color, w = 130, h = 34 }: { data: number[]; color: string; w?: number; h?: number }) {
const min = Math.min(...data);
const max = Math.max(...data);
const span = max - min || 1;
const px = (i: number) => 3 + (i / (data.length - 1)) * (w - 10);
const py = (v: number) => h - 5 - ((v - min) / span) * (h - 10);
const path = data.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' ');
return (
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} aria-hidden="true">
<path d={path} fill="none" stroke={color} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
<circle cx={px(data.length - 1)} cy={py(data[data.length - 1])} r="4" fill={color} stroke="var(--surface)" strokeWidth="2" />
</svg>
);
}
/** Diverging R-multiple bar: grows from a centered zero baseline. */
function RBar({ r, max = 1.6 }: { r: number; max?: number }) {
const pct = Math.min(Math.abs(r) / max, 1) * 50;
return (
<div className="hzn-rbar" role="img" aria-label={`${signed(r)} R`}>
<span className="hzn-rbar-zero" />
<span
className="hzn-rbar-fill"
style={{
left: r >= 0 ? '50%' : `${50 - pct}%`,
width: `${pct}%`,
background: r >= 0 ? 'var(--up)' : 'var(--down)',
borderRadius: r >= 0 ? '0 4px 4px 0' : '4px 0 0 4px',
}}
/>
</div>
);
}
/* ------------------------------------------------------------------ */
/* Radar — the score fingerprint (5 axes, 0100 %ile, fixed order) */
/* ------------------------------------------------------------------ */
function RadarChart({
scores, reference, size = 110, labels = false,
}: { scores: number[]; reference?: number[]; size?: number; labels?: boolean }) {
const [hover, setHover] = useState<number | null>(null);
const pad = labels ? 78 : 10;
const vb = size + pad * 2;
const c = vb / 2;
const r = size / 2;
const pt = (v: number, i: number): [number, number] => {
const a = ((-90 + i * 72) * Math.PI) / 180;
return [c + (Math.cos(a) * r * v) / 100, c + (Math.sin(a) * r * v) / 100];
};
const poly = (vals: number[]) =>
vals.map((v, i) => pt(v, i).map((n) => n.toFixed(1)).join(',')).join(' ');
const rendered = vb * (labels ? 0.95 : 1);
const tip = hover !== null ? pt(scores[hover], hover) : null;
return (
<div className="hzn-radar-wrap" style={{ width: rendered, height: rendered }}>
<svg
viewBox={`0 0 ${vb} ${vb}`}
width={rendered}
height={rendered}
role="img"
aria-label={`Score fingerprint: ${RADAR_AXES.map((ax, i) => `${ax} ${scores[i]}`).join(', ')} (0100 percentile)`}
>
{[50, 100].map((ring) => (
<polygon key={ring} points={poly(Array(5).fill(ring))} fill="none" stroke="var(--grid)" strokeWidth="1" />
))}
{RADAR_AXES.map((ax, i) => {
const [x, y] = pt(100, i);
return <line key={ax} x1={c} y1={c} x2={x} y2={y} stroke="var(--grid)" strokeWidth="1" />;
})}
{reference && (
<polygon points={poly(reference)} fill="none" stroke="var(--ink-3)" strokeWidth="1.5" opacity="0.85" />
)}
<polygon
points={poly(scores)}
fill="var(--up)" fillOpacity="0.13"
stroke="var(--up)" strokeWidth="2" strokeLinejoin="round"
/>
{scores.map((v, i) => {
const [x, y] = pt(v, i);
return (
<circle
key={i} cx={x} cy={y} r={hover === i ? 4.5 : 3}
fill="var(--up)" stroke="var(--surface)" strokeWidth="1.5"
/>
);
})}
{labels && RADAR_AXES.map((ax, i) => {
const a = ((-90 + i * 72) * Math.PI) / 180;
const x = c + Math.cos(a) * (r + 14);
const y = c + Math.sin(a) * (r + 14);
const anchor = Math.cos(a) > 0.3 ? 'start' : Math.cos(a) < -0.3 ? 'end' : 'middle';
const dy = Math.sin(a) < -0.3 ? -2 : Math.sin(a) > 0.3 ? 9 : 4;
return (
<text key={ax} x={x} y={y + dy} textAnchor={anchor} className="hzn-radar-axis">
{ax} <tspan className="hzn-radar-axisval">{scores[i]}</tspan>
</text>
);
})}
{/* hit targets on top — comfortably larger than the dots, keyboard-reachable */}
{scores.map((v, i) => {
const [x, y] = pt(v, i);
return (
<circle
key={`hit-${i}`} cx={x} cy={y} r="13"
fill="transparent" style={{ cursor: 'default', outline: 'none' }}
tabIndex={0}
aria-label={`${RADAR_AXES_FULL[i]}: ${scores[i]}th percentile${reference ? `, live-setup median ${reference[i]}` : ''}`}
onMouseEnter={() => setHover(i)}
onMouseLeave={() => setHover(null)}
onFocus={() => setHover(i)}
onBlur={() => setHover(null)}
/>
);
})}
</svg>
{hover !== null && tip && (
<div
className="hzn-radar-tip"
style={{ left: `${(tip[0] / vb) * 100}%`, top: `${(tip[1] / vb) * 100}%` }}
>
<b>{RADAR_AXES_FULL[hover]}</b>
<span className="num">{scores[hover]}th %ile</span>
{reference && <i className="num">live-setup median {reference[hover]}</i>}
</div>
)}
</div>
);
}
/* ------------------------------------------------------------------ */
/* Trade chart — price since entry with entry / trailing stop / target */
/* ------------------------------------------------------------------ */
type Position = (typeof positions)[number];
function TradeChart({ p }: { p: Position }) {
const w = 560; const h = 150;
const padL = 8; const padR = 96; const padT = 10; const padB = 12;
const isShort = p.direction === 'short';
const vals = [...p.spark, ...p.stopTrail, p.entry];
let lo = Math.min(...vals);
let hi = Math.max(...vals);
const range = hi - lo || 1;
// Only stretch the scale to the target when it doesn't flatten the price action.
const targetFits = isShort ? lo - p.target <= range * 0.9 : p.target - hi <= range * 0.9;
if (targetFits) { lo = Math.min(lo, p.target); hi = Math.max(hi, p.target); }
const pad = (hi - lo) * 0.07;
lo -= pad; hi += pad;
const plotW = w - padL - padR;
const plotH = h - padT - padB;
const px = (i: number) => padL + (i / (p.spark.length - 1)) * plotW;
const py = (v: number) => padT + plotH - ((v - lo) / (hi - lo)) * plotH;
const pricePath = p.spark.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' ');
let stopPath = `M${px(0).toFixed(1)},${py(p.stopTrail[0]).toFixed(1)}`;
for (let i = 1; i < p.stopTrail.length; i++) {
stopPath += `H${px(i).toFixed(1)}V${py(p.stopTrail[i]).toFixed(1)}`;
}
const col = p.r >= 0 ? 'var(--up)' : 'var(--down)';
const labelX = w - padR + 10;
const entryY = py(p.entry);
let stopY = py(p.stopTrail[p.stopTrail.length - 1]);
if (Math.abs(stopY - entryY) < 11) stopY = entryY + (stopY >= entryY ? 11 : -11);
const lastI = p.spark.length - 1;
return (
<svg
viewBox={`0 0 ${w} ${h}`}
className="hzn-tradechart"
role="img"
aria-label={`${p.symbol}: price ${fmt(p.current)}, entry ${fmt(p.entry)}, trailing stop ${fmt(p.stop)}, target ${fmt(p.target)}`}
>
<line x1={padL} x2={w - padR} y1={entryY} y2={entryY} stroke="var(--ink-3)" strokeWidth="1" opacity="0.5" />
<text x={labelX} y={entryY + 3.5} className="hzn-lvl">entry {fmt(p.entry)}</text>
<path d={stopPath} fill="none" stroke="var(--down)" strokeWidth="1.5" opacity="0.85" />
<text x={labelX} y={stopY + 3.5} className="hzn-lvl hzn-lvl-down">stop {fmt(p.stop)}</text>
{targetFits ? (
<>
<line x1={padL} x2={w - padR} y1={py(p.target)} y2={py(p.target)} stroke="var(--up)" strokeWidth="1" opacity="0.6" />
<text x={labelX} y={py(p.target) + 3.5} className="hzn-lvl hzn-lvl-up">target {fmt(p.target)}</text>
</>
) : (
<text x={labelX} y={isShort ? h - padB : padT + 4} className="hzn-lvl hzn-lvl-up">
target {fmt(p.target)} {isShort ? '↓' : '↑'}
</text>
)}
<path d={pricePath} fill="none" stroke={col} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
<circle cx={px(lastI)} cy={py(p.spark[lastI])} r="4" fill={col} stroke="var(--surface)" strokeWidth="2" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* The price rail — stop → entry → current → target, laid out spatially */
/* ------------------------------------------------------------------ */
function PriceRail({ s }: { s: typeof heroSetup }) {
const lo = s.stop - (s.target - s.stop) * 0.06;
const hi = s.target + (s.target - s.stop) * 0.06;
const pct = (v: number) => ((v - lo) / (hi - lo)) * 100;
const rNow = (s.current - s.entry) / (s.entry - s.stop);
return (
<div className="hzn-rail">
<div className="hzn-rail-track" />
<div
className="hzn-rail-risk"
style={{ left: `${pct(s.stop)}%`, width: `${pct(s.entry) - pct(s.stop)}%` }}
/>
<div
className="hzn-rail-progress"
style={{ left: `${pct(s.entry)}%`, width: `${pct(s.current) - pct(s.entry)}%` }}
/>
{/* markers */}
<div className="hzn-rail-mark" style={{ left: `${pct(s.stop)}%` }}>
<span className="hzn-rail-tick" style={{ background: 'var(--down)' }} />
<span className="hzn-rail-label">
<em>stop</em>
<b>{fmt(s.stop)}</b>
<i>1R</i>
</span>
</div>
<div className="hzn-rail-mark" style={{ left: `${pct(s.entry)}%` }}>
<span className="hzn-rail-tick" style={{ background: 'var(--ink-2)' }} />
<span className="hzn-rail-label">
<em>entry</em>
<b>{fmt(s.entry)}</b>
</span>
</div>
<div className="hzn-rail-mark hzn-rail-now" style={{ left: `${pct(s.current)}%` }}>
<span className="hzn-rail-dot" />
<span className="hzn-rail-label hzn-rail-label-top">
<em>now</em>
<b>{fmt(s.current)}</b>
<i>{signed(rNow)}R</i>
</span>
</div>
<div className="hzn-rail-mark" style={{ left: `${pct(s.target)}%` }}>
<span className="hzn-rail-ring" />
<span className="hzn-rail-label">
<em>target</em>
<b>{fmt(s.target)}</b>
<i>+{fmt(s.rr, 1)}R</i>
</span>
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
/* Performance chart — indexed to 100, crosshair + tooltip on hover */
/* ------------------------------------------------------------------ */
const CHART = { w: 760, h: 220, top: 14, right: 64, bottom: 26, left: 40 };
function PerfChart() {
const [hover, setHover] = useState<number | null>(null);
const svgRef = useRef<SVGSVGElement>(null);
const { w, h, top, right, bottom, left } = CHART;
const plotW = w - left - right;
const plotH = h - top - bottom;
const yMin = 95;
const yMax = 130;
const px = (i: number) => left + (i / (perfPortfolio.length - 1)) * plotW;
const py = (v: number) => top + plotH - ((v - yMin) / (yMax - yMin)) * plotH;
const line = (data: number[]) =>
data.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' ');
const yTicks = [100, 110, 120, 130];
const xTicks = useMemo(
() => [0, 5, 10, 15, 20, 25].map((i) => ({ i, label: perfDates[i] })),
[],
);
const onMove = (e: React.MouseEvent<SVGSVGElement>) => {
const rect = svgRef.current?.getBoundingClientRect();
if (!rect) return;
const x = ((e.clientX - rect.left) / rect.width) * w;
const i = Math.round(((x - left) / plotW) * (perfPortfolio.length - 1));
setHover(Math.max(0, Math.min(perfPortfolio.length - 1, i)));
};
const last = perfPortfolio.length - 1;
return (
<div className="hzn-chart">
<div className="hzn-chart-head">
<div>
<p className="hzn-eyebrow">Performance</p>
<p className="hzn-chart-title">Paper book vs S&amp;P 500 · indexed to 100 · since January</p>
</div>
<div className="hzn-legend">
<span><i style={{ background: 'var(--up)' }} /> Book</span>
<span><i style={{ background: 'var(--ink-3)' }} /> S&amp;P 500</span>
</div>
</div>
<svg
ref={svgRef}
viewBox={`0 0 ${w} ${h}`}
className="hzn-chart-svg"
onMouseMove={onMove}
onMouseLeave={() => setHover(null)}
role="img"
aria-label="Paper book up 26.4% versus S&P 500 up 8.2% since January, indexed to 100"
>
{yTicks.map((t) => (
<g key={t}>
<line x1={left} x2={w - right} y1={py(t)} y2={py(t)} stroke="var(--grid)" strokeWidth="1" />
<text x={left - 8} y={py(t) + 3.5} textAnchor="end" className="hzn-axis">{t}</text>
</g>
))}
{xTicks.map(({ i, label }) => (
<text key={i} x={px(i)} y={h - 6} textAnchor="middle" className="hzn-axis">{label}</text>
))}
<path d={line(perfSpy)} fill="none" stroke="var(--ink-3)" strokeWidth="2" strokeLinejoin="round" />
<path d={line(perfPortfolio)} fill="none" stroke="var(--up)" strokeWidth="2" strokeLinejoin="round" />
{hover !== null && (
<g>
<line x1={px(hover)} x2={px(hover)} y1={top} y2={top + plotH} stroke="var(--ink-3)" strokeWidth="1" />
<circle cx={px(hover)} cy={py(perfPortfolio[hover])} r="4.5" fill="var(--up)" stroke="var(--surface)" strokeWidth="2" />
<circle cx={px(hover)} cy={py(perfSpy[hover])} r="4.5" fill="var(--ink-3)" stroke="var(--surface)" strokeWidth="2" />
</g>
)}
{/* direct end labels */}
<circle cx={px(last)} cy={py(perfPortfolio[last])} r="4" fill="var(--up)" stroke="var(--surface)" strokeWidth="2" />
<circle cx={px(last)} cy={py(perfSpy[last])} r="4" fill="var(--ink-3)" stroke="var(--surface)" strokeWidth="2" />
<text x={px(last) + 10} y={py(perfPortfolio[last]) + 4} className="hzn-endlabel">{fmt(perfPortfolio[last], 1)}</text>
<text x={px(last) + 10} y={py(perfSpy[last]) + 4} className="hzn-endlabel hzn-endlabel-dim">{fmt(perfSpy[last], 1)}</text>
</svg>
<div className="hzn-chart-tip" aria-live="polite">
{hover !== null
? <>week of {perfDates[hover]} book <b>{fmt(perfPortfolio[hover], 1)}</b> · S&amp;P 500 <b>{fmt(perfSpy[hover], 1)}</b></>
: <>hover for weekly values</>}
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
/* Ticker drill-down — candlesticks + volume + trade levels */
/* ------------------------------------------------------------------ */
function TickerPanel() {
const [hover, setHover] = useState<number | null>(null);
const svgRef = useRef<SVGSVGElement>(null);
const w = 900; const padT = 10; const padB = 20; const padL = 8; const padR = 96;
const priceH = 290; const volH = 54; const gap = 16;
const H = padT + priceH + gap + volH + padB;
const n = vrtCandles.length;
const slot = (w - padL - padR) / n;
const bodyW = Math.min(7, slot * 0.62);
const hi = Math.max(...vrtCandles.map((b) => b.h), heroSetup.target + 1.5);
const lo = Math.min(...vrtCandles.map((b) => b.l)) - 1;
const maxV = Math.max(...vrtCandles.map((b) => b.v));
const px = (i: number) => padL + (i + 0.5) * slot;
const py = (v: number) => padT + priceH - ((v - lo) / (hi - lo)) * priceH;
const vy = (v: number) => padT + priceH + gap + volH - (v / maxV) * volH;
// Skip ticks whose right-gutter label would collide with a level label.
const levelYs = [
heroSetup.entry, heroSetup.stop, heroSetup.target,
tickerLevels.resistance, (tickerLevels.supportLo + tickerLevels.supportHi) / 2,
];
const yTicks = [75, 85, 95, 105].filter(
(t) => t > lo && t < hi
&& levelYs.every((l) => Math.abs(((t - l) / (hi - lo)) * priceH) >= 13),
);
const monthTicks = vrtCandles
.map((b, i) => ({ b, i }))
.filter(({ b, i }) => i === 0 || b.d.getMonth() !== vrtCandles[i - 1].d.getMonth())
.map(({ b, i }) => ({ i, label: b.d.toLocaleDateString('en-US', { month: 'short' }) }));
const onMove = (e: React.MouseEvent<SVGSVGElement>) => {
const rect = svgRef.current?.getBoundingClientRect();
if (!rect) return;
const x = ((e.clientX - rect.left) / rect.width) * w;
const i = Math.floor((x - padL) / slot);
setHover(Math.max(0, Math.min(n - 1, i)));
};
const labelX = w - padR + 10;
const hb = hover !== null ? vrtCandles[hover] : null;
return (
<section className="hzn-chart hzn-ticker" aria-label="Ticker drill-down: VRT">
<div className="hzn-ticker-top">
<div>
<p className="hzn-eyebrow">Drill-down · Ticker</p>
<p className="hzn-tk-title">
<b>VRT</b>
<span>Vertiv Holdings</span>
</p>
<p className="hzn-tk-price">
<span className="num">94.30</span>
<b className="hzn-up num">+1.9%</b>
<i>today</i>
</p>
<div className="hzn-tk-chips">
<span className="hzn-chip">composite score 82</span>
<span className="hzn-chip">ATR 2.90 · 3.1%</span>
<span className="hzn-chip">earnings Aug 5</span>
</div>
</div>
<div className="hzn-tk-radar">
<RadarChart scores={heroScores} reference={liveMedian} size={128} labels />
<p className="hzn-legend">
<span><i style={{ background: 'var(--up)' }} /> VRT</span>
<span><i style={{ background: 'var(--ink-3)' }} /> live-setup median</span>
</p>
</div>
</div>
<div className="hzn-tk-bars">
<nav className="hzn-tk-tabs" aria-label="Ticker sections">
<a className="hzn-tk-tab-on" href="#">Analysis</a>
<a href="#">Indicators</a>
<a href="#">S/R levels</a>
<a href="#">Sentiment</a>
<a href="#">Fundamentals</a>
</nav>
<div className="hzn-tk-ranges" role="group" aria-label="Time range">
{(['1M', '3M', '6M', 'YTD', '1Y', 'All'] as const).map((r) => (
<button key={r} className={r === '3M' ? 'hzn-tk-range-on' : ''}>{r}</button>
))}
</div>
</div>
<svg
ref={svgRef}
viewBox={`0 0 ${w} ${H}`}
className="hzn-chart-svg"
onMouseMove={onMove}
onMouseLeave={() => setHover(null)}
role="img"
aria-label="Daily candlestick chart for VRT with entry, trailing stop, target, support and resistance levels"
>
{yTicks.map((t) => (
<g key={t}>
<line x1={padL} x2={w - padR} y1={py(t)} y2={py(t)} stroke="var(--grid)" strokeWidth="1" />
<text x={labelX} y={py(t) + 3.5} className="hzn-axis">{t}</text>
</g>
))}
{monthTicks.map(({ i, label }) => (
<text key={label + i} x={px(i)} y={H - 5} textAnchor="middle" className="hzn-axis">{label}</text>
))}
{/* support zone + resistance (nearest S/R only — full list stays in its tab) */}
<rect
x={padL} width={w - padR - padL}
y={py(tickerLevels.supportHi)}
height={py(tickerLevels.supportLo) - py(tickerLevels.supportHi)}
fill="var(--up)" opacity="0.07"
/>
<text x={labelX} y={py((tickerLevels.supportLo + tickerLevels.supportHi) / 2) + 3.5} className="hzn-lvl">
sup {fmt(tickerLevels.supportLo, 1)}{fmt(tickerLevels.supportHi, 1)}
</text>
<line x1={padL} x2={w - padR} y1={py(tickerLevels.resistance)} y2={py(tickerLevels.resistance)} stroke="var(--ink-3)" strokeWidth="1" opacity="0.7" />
<text x={labelX} y={py(tickerLevels.resistance) + 3.5} className="hzn-lvl">resist {fmt(tickerLevels.resistance)}</text>
{/* trade levels */}
<line x1={padL} x2={w - padR} y1={py(heroSetup.entry)} y2={py(heroSetup.entry)} stroke="var(--ink-2)" strokeWidth="1" opacity="0.55" />
<text x={labelX} y={py(heroSetup.entry) + 3.5} className="hzn-lvl hzn-lvl-ink">entry {fmt(heroSetup.entry)}</text>
<line x1={padL} x2={w - padR} y1={py(heroSetup.stop)} y2={py(heroSetup.stop)} stroke="var(--down)" strokeWidth="1" opacity="0.8" />
<text x={labelX} y={py(heroSetup.stop) + 3.5} className="hzn-lvl hzn-lvl-down">stop {fmt(heroSetup.stop)}</text>
<line x1={padL} x2={w - padR} y1={py(heroSetup.target)} y2={py(heroSetup.target)} stroke="var(--up)" strokeWidth="1" opacity="0.8" />
<text x={labelX} y={py(heroSetup.target) + 3.5} className="hzn-lvl hzn-lvl-up">target {fmt(heroSetup.target)}</text>
{/* candles */}
{vrtCandles.map((b, i) => {
const up = b.c >= b.o;
const col = up ? 'var(--up)' : 'var(--down)';
const bodyY = py(Math.max(b.o, b.c));
const bodyH = Math.max(1.5, Math.abs(py(b.o) - py(b.c)));
return (
<g key={i} opacity={hover === null || hover === i ? 1 : 0.55}>
<line x1={px(i)} x2={px(i)} y1={py(b.h)} y2={py(b.l)} stroke={col} strokeWidth="1" />
<rect x={px(i) - bodyW / 2} y={bodyY} width={bodyW} height={bodyH} fill={col} rx="1" />
<rect x={px(i) - bodyW / 2} y={vy(b.v)} width={bodyW} height={padT + priceH + gap + volH - vy(b.v)} fill="var(--ink-3)" opacity="0.55" rx="1" />
</g>
);
})}
{hover !== null && (
<line x1={px(hover)} x2={px(hover)} y1={padT} y2={padT + priceH + gap + volH} stroke="var(--ink-3)" strokeWidth="1" opacity="0.6" />
)}
</svg>
<div className="hzn-chart-tip" aria-live="polite">
{hb ? (
<>
{hb.d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} O <b>{fmt(hb.o)}</b> H <b>{fmt(hb.h)}</b> L <b>{fmt(hb.l)}</b> C <b>{fmt(hb.c)}</b> · vol <b>{(hb.v / 1e6).toFixed(1)}M</b>
</>
) : (
<>hover for daily OHLC · volume below</>
)}
</div>
<p className="hzn-fresh">
<span><i /> prices 2h ago</span>
<span><i /> indicators 2h ago</span>
<span><i /> sentiment 6h ago</span>
<span><i /> fundamentals 3d ago</span>
<a href="#">manage data </a>
</p>
</section>
);
}
/* ------------------------------------------------------------------ */
/* Page */
/* ------------------------------------------------------------------ */
export default function DesignHorizonPage() {
const [expanded, setExpanded] = useState<string | null>('ANET');
const [showTicker, setShowTicker] = useState(true);
return (
<div className="hzn">
<style>{CSS}</style>
{/* atmosphere — stays behind everything, never carries data */}
<div className="hzn-stars" aria-hidden="true" />
<div className="hzn-horizon" aria-hidden="true">
<div className="hzn-rim" />
<div className="hzn-planet" />
</div>
<div className="hzn-page">
{/* command bar */}
<header className="hzn-bar">
<span className="hzn-wordmark">SIGNAL<i /></span>
<nav className="hzn-nav" aria-label="Sections">
<a className="hzn-nav-on" href="#">Overview</a>
<a href="#">Market</a>
<a href="#">Signals</a>
<a href="#">Regime</a>
</nav>
<div className="hzn-bar-right">
<span className="hzn-regime"><i /> Neutral regime · SPY above 50-day, breadth mixed</span>
<span className="hzn-mock">mockup · static data</span>
</div>
</header>
{/* the verdict */}
<section className="hzn-hero">
<p className="hzn-eyebrow">Thursday, July 9</p>
<h1>One setup clears the gate today.</h1>
<p className="hzn-hero-sub">
3 qualified of 14 live setups · 4 open positions · <b className="hzn-up">+2.3R</b> unrealized
</p>
</section>
{/* focal setup card */}
<section className="hzn-focus" aria-label="Top qualified setup">
<div className="hzn-focus-head">
<div className="hzn-focus-id">
<span className="hzn-pick">top pick</span>
<h2>{heroSetup.symbol}</h2>
<div>
<p className="hzn-focus-name">{heroSetup.name}</p>
<p className="hzn-focus-tags">
<DirTag direction={heroSetup.direction} />
<span className="hzn-chip">conviction {heroSetup.conviction}</span>
<span className="hzn-chip">residual momentum {heroSetup.residualPct}th %ile</span>
</p>
</div>
</div>
<div className="hzn-focus-stats">
<div>
<p className="hzn-stat-label">reward / risk</p>
<p className="hzn-stat-value">{fmt(heroSetup.rr, 1)}<span> : 1</span></p>
</div>
<div>
<p className="hzn-stat-label">target probability</p>
<p className="hzn-stat-value">{heroSetup.targetProb}<span>%</span></p>
<div className="hzn-meter"><span style={{ width: `${heroSetup.targetProb}%` }} /></div>
</div>
</div>
</div>
<PriceRail s={heroSetup} />
<div className="hzn-focus-foot">
<span className="hzn-note">cleared the expected-value gate · {heroSetup.sizeHint}</span>
<span className="hzn-actions">
<button className="hzn-btn" onClick={() => setShowTicker((v) => !v)} aria-expanded={showTicker}>
{showTicker ? 'Hide ticker details' : 'Ticker details'}
</button>
<a className="hzn-btn hzn-btn-ghost" href="#">All 14 setups</a>
</span>
</div>
</section>
{showTicker && <TickerPanel />}
{/* KPI band */}
<section className="hzn-kpis" aria-label="Account status">
<div className="hzn-tile">
<p className="hzn-stat-label">open risk</p>
<p className="hzn-stat-value">$1,240</p>
<p className="hzn-tile-sub">4 open · distance to stops</p>
</div>
<div className="hzn-tile">
<p className="hzn-stat-label">unrealized</p>
<p className="hzn-stat-value hzn-up">+2.3R</p>
<p className="hzn-tile-sub">+$1,875 · 3 up, 1 down</p>
</div>
<div className="hzn-tile hzn-tile-spark">
<div>
<p className="hzn-stat-label">alpha vs S&amp;P 500</p>
<p className="hzn-stat-value hzn-up">+$642</p>
<p className="hzn-tile-sub">open trades vs buy-and-hold SPY</p>
</div>
<Spark data={alphaSpark} color="var(--up)" />
</div>
<div className="hzn-tile">
<p className="hzn-stat-label">gate this week</p>
<p className="hzn-stat-value">3<span className="hzn-stat-dim"> / 14</span></p>
<p className="hzn-tile-sub">setups clearing the EV gate</p>
</div>
</section>
<div className="hzn-cols">
{/* open positions — diverging R bars, expandable rows */}
<section className="hzn-panel" aria-label="Open positions">
<div className="hzn-panel-head">
<p className="hzn-eyebrow">Open positions</p>
<p className="hzn-panel-hint">R-multiple to entry · select a row for detail</p>
</div>
<ul className="hzn-poslist">
{positions.map((p) => {
const open = expanded === p.symbol;
return (
<li key={p.symbol} className={open ? 'hzn-pos-open' : ''}>
<button
className="hzn-pos"
onClick={() => setExpanded(open ? null : p.symbol)}
aria-expanded={open}
>
<span className="hzn-pos-id">
<b>{p.symbol}</b>
<DirTag direction={p.direction} />
</span>
<span className="hzn-pos-px num">{fmt(p.entry)} {fmt(p.current)}</span>
<RBar r={p.r} />
<span className={`hzn-pos-r num ${p.r >= 0 ? 'hzn-up' : 'hzn-down'}`}>{signed(p.r)}R</span>
<span className={`hzn-caret ${open ? 'hzn-caret-open' : ''}`} aria-hidden="true"></span>
</button>
{open && (
<div className="hzn-pos-detail">
<dl>
<div><dt>opened</dt><dd>{p.opened} · {p.shares} shares</dd></div>
<div><dt>stop</dt><dd className="num">{fmt(p.stop)} <i>{p.stopNote}</i></dd></div>
<div><dt>target</dt><dd className="num">{fmt(p.target)}</dd></div>
<div><dt>exit rule</dt><dd>{p.exit}</dd></div>
<div>
<dt>alpha vs SPY</dt>
<dd className={`num ${p.alphaUsd >= 0 ? 'hzn-up' : 'hzn-down'}`}>{`${p.alphaUsd >= 0 ? '+' : ''}$${fmt(Math.abs(p.alphaUsd), 0)}`}</dd>
</div>
</dl>
<div className="hzn-pos-chart">
<p className="hzn-stat-label">since entry · entry / trailing stop / target</p>
<TradeChart p={p} />
</div>
</div>
)}
</li>
);
})}
</ul>
</section>
{/* radar — ranked next setups with reasons */}
<section className="hzn-panel" aria-label="Setup radar">
<div className="hzn-panel-head">
<p className="hzn-eyebrow">Radar</p>
<p className="hzn-panel-hint">ranked by strategy score · why each does or doesn't qualify</p>
</div>
<div className="hzn-cmp-row">
{qualifiedCompare.map((q) => (
<figure key={q.symbol} className="hzn-cmp-fig">
<RadarChart scores={q.scores} size={96} />
<figcaption>
<b>{q.symbol}</b>
<span className={q.tag === 'top pick' ? 'hzn-cmp-pick' : ''}>{q.tag}</span>
</figcaption>
</figure>
))}
<p className="hzn-cmp-axes">
the 3 qualified setups<br />
same axes as the ticker fingerprint · 0100 %ile
</p>
</div>
<ul className="hzn-radar">
{radar.map((r) => (
<li key={r.symbol} className={r.qualified ? '' : 'hzn-radar-dim'}>
<span className="hzn-radar-rank num">{r.rank}</span>
<span className="hzn-radar-id">
<b>{r.symbol}</b>
<small>{r.name}</small>
</span>
<DirTag direction={r.direction} />
<span className="hzn-radar-mom">
<span className="hzn-radar-momtrack">
<i style={{ width: `${r.residual}%`, background: r.qualified ? 'var(--up)' : 'var(--ink-3)' }} />
</span>
<small className="num">{r.residual}%ile</small>
</span>
<span className="hzn-radar-rr num">{r.rr}</span>
<span className={`hzn-radar-note ${r.qualified ? 'hzn-radar-yes' : ''}`}>
{r.qualified ? ' ' : ''}{r.note}
</span>
</li>
))}
</ul>
</section>
</div>
<PerfChart />
{/* watchlist strip */}
<section className="hzn-watch" aria-label="Watchlist">
<p className="hzn-eyebrow">Watchlist</p>
<div className="hzn-watch-chips">
{watchlist.map((c) => (
<a key={c.symbol} href="#" className="hzn-wchip">
<b>{c.symbol}</b>
<span className={`num ${c.pct > 0.05 ? 'hzn-up' : c.pct < -0.05 ? 'hzn-down' : ''}`}>
{c.pct > 0 ? '+' : ''}{fmt(c.pct)}%
</span>
</a>
))}
</div>
</section>
<footer className="hzn-foot">
HORIZON · design study v2 · static mock data, nothing live
</footer>
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
/* Styles — scoped under .hzn, independent of the app theme */
/* ------------------------------------------------------------------ */
const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
.hzn {
/* tokens — chart colors validated on the dark surface (#11131c) */
--void: #0a0b11;
--surface: #11131c;
--panel: rgba(226, 231, 245, 0.032);
--panel-glass: rgba(15, 17, 26, 0.72);
--panel-line: rgba(226, 231, 245, 0.085);
--grid: rgba(226, 231, 245, 0.07);
--ink: #edeef3;
--ink-2: #9aa0b0;
--ink-3: #5d6373;
--up: #2f9db2; /* marks */
--up-text: #6ec9db; /* small text tint */
--down: #e36a58;
--down-text: #ef9182;
--amber: #c67f22;
--amber-text: #dfa356;
--ember: #ff6a45; /* reserved: horizon, wordmark, top-pick */
position: relative;
z-index: 0;
min-height: 100vh;
background: var(--void);
color: var(--ink);
font-family: 'Instrument Sans', system-ui, sans-serif;
font-size: 15px;
line-height: 1.5;
overflow-x: hidden;
}
.hzn h1, .hzn h2 { font-family: 'Space Grotesk', system-ui, sans-serif; }
.hzn .num { font-family: 'IBM Plex Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; }
.hzn a { color: inherit; text-decoration: none; }
.hzn :focus-visible { outline: 2px solid var(--up-text); outline-offset: 2px; border-radius: 4px; }
/* ------- atmosphere ------- */
.hzn-stars {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background-image:
radial-gradient(1px 1px at 12% 18%, rgba(237,238,243,.55) 50%, transparent 51%),
radial-gradient(1px 1px at 68% 8%, rgba(237,238,243,.4) 50%, transparent 51%),
radial-gradient(1.5px 1.5px at 84% 32%, rgba(159,216,232,.5) 50%, transparent 51%),
radial-gradient(1px 1px at 33% 44%, rgba(237,238,243,.35) 50%, transparent 51%),
radial-gradient(1px 1px at 91% 64%, rgba(237,238,243,.3) 50%, transparent 51%),
radial-gradient(1.5px 1.5px at 22% 71%, rgba(237,238,243,.42) 50%, transparent 51%),
radial-gradient(1px 1px at 55% 27%, rgba(255,150,120,.35) 50%, transparent 51%),
radial-gradient(1px 1px at 45% 87%, rgba(237,238,243,.3) 50%, transparent 51%),
radial-gradient(1px 1px at 76% 82%, rgba(159,216,232,.35) 50%, transparent 51%),
radial-gradient(1px 1px at 5% 55%, rgba(237,238,243,.35) 50%, transparent 51%);
background-size: 900px 700px;
background-repeat: repeat;
opacity: .8;
}
.hzn-horizon {
position: fixed; left: 0; right: 0; bottom: 0; height: 24vh;
z-index: 0; pointer-events: none; overflow: hidden;
}
.hzn-planet {
position: absolute; left: 50%; top: 58%;
width: 300vw; height: 300vw; transform: translateX(-50%);
border-radius: 50%;
background:
radial-gradient(ellipse 55% 14% at 50% 0.4%, rgba(255,120,80,.22), transparent 60%),
radial-gradient(ellipse 120% 26% at 50% 0%, #3f130b 0%, #260a06 32%, #130504 58%, #0a0b11 86%);
}
.hzn-rim {
position: absolute; left: 50%; top: calc(58% - 2px);
width: 300vw; height: 300vw; transform: translateX(-50%);
border-radius: 50%;
background: linear-gradient(180deg, rgba(213,240,250,.55), rgba(159,216,232,.16) 40%, transparent 60%);
filter: blur(1px);
box-shadow: 0 -14px 50px rgba(159,216,232,.10), 0 -50px 140px rgba(228,87,61,.08);
}
/* ------- layout ------- */
.hzn-page {
position: relative; z-index: 1;
max-width: 1180px; margin: 0 auto;
padding: 28px 32px 22vh;
display: flex; flex-direction: column; gap: 34px;
}
/* ------- command bar ------- */
.hzn-bar { display: flex; align-items: center; gap: 28px; flex-wrap: wrap; }
.hzn-wordmark {
font-family: 'Space Grotesk', sans-serif; font-weight: 700;
letter-spacing: .42em; font-size: 15px;
}
.hzn-wordmark i {
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
background: var(--ember); margin-left: 2px;
box-shadow: 0 0 10px rgba(255,106,69,.7);
}
.hzn-nav { display: flex; gap: 22px; font-size: 13.5px; color: var(--ink-2); }
.hzn-nav a:hover { color: var(--ink); }
.hzn-nav-on { color: var(--ink); border-bottom: 1px solid var(--up); padding-bottom: 2px; }
.hzn-bar-right { margin-left: auto; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.hzn-regime { font-size: 12.5px; color: var(--ink-2); display: inline-flex; align-items: center; gap: 8px; }
.hzn-regime i { width: 7px; height: 7px; border-radius: 50%; background: var(--amber-text); display: inline-block; }
.hzn-mock {
font-family: 'IBM Plex Mono', monospace; font-size: 10px; letter-spacing: .14em;
text-transform: uppercase; color: var(--ink-3);
border: 1px solid var(--panel-line); border-radius: 99px; padding: 3px 10px;
}
/* ------- hero ------- */
.hzn-eyebrow {
font-family: 'IBM Plex Mono', monospace; font-size: 10.5px;
letter-spacing: .28em; text-transform: uppercase; color: var(--ink-3);
}
.hzn-hero { margin-top: 6px; }
.hzn-hero h1 {
font-size: clamp(28px, 4.2vw, 44px); font-weight: 600;
letter-spacing: -0.015em; margin-top: 10px;
}
.hzn-hero-sub { margin-top: 8px; color: var(--ink-2); font-size: 15px; }
.hzn-up { color: var(--up-text); }
.hzn-down { color: var(--down-text); }
/* ------- focal setup ------- */
.hzn-focus {
background: var(--panel-glass); border: 1px solid var(--panel-line);
border-radius: 16px; padding: 26px 28px 22px;
backdrop-filter: blur(14px);
}
.hzn-focus-head { display: flex; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
.hzn-focus-id { display: flex; align-items: flex-start; gap: 18px; }
.hzn-pick {
font-family: 'IBM Plex Mono', monospace; font-size: 9.5px; letter-spacing: .2em;
text-transform: uppercase; color: var(--ember);
border: 1px solid rgba(255,106,69,.4); border-radius: 99px;
padding: 3px 9px; margin-top: 10px; white-space: nowrap;
}
.hzn-focus h2 { font-size: 46px; font-weight: 700; letter-spacing: -0.01em; line-height: 1; }
.hzn-focus-name { color: var(--ink-2); font-size: 13.5px; margin-top: 4px; }
.hzn-focus-tags { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.hzn-tag {
font-family: 'IBM Plex Mono', monospace; font-size: 9.5px; font-weight: 600;
letter-spacing: .12em; text-transform: uppercase;
border-radius: 4px; padding: 2.5px 7px;
}
.hzn-tag-long { color: var(--up-text); background: rgba(47,157,178,.14); }
.hzn-tag-short { color: var(--down-text); background: rgba(227,106,88,.14); }
.hzn-chip {
font-size: 11.5px; color: var(--ink-2);
border: 1px solid var(--panel-line); border-radius: 99px; padding: 2.5px 10px;
}
.hzn-focus-stats { display: flex; gap: 40px; text-align: right; }
.hzn-stat-label {
font-family: 'IBM Plex Mono', monospace; font-size: 10px;
letter-spacing: .18em; text-transform: uppercase; color: var(--ink-3);
}
.hzn-stat-value {
font-family: 'Space Grotesk', sans-serif; font-size: 30px; font-weight: 600;
margin-top: 4px; line-height: 1.1;
}
.hzn-stat-value span { font-size: 17px; color: var(--ink-2); }
.hzn-stat-dim { color: var(--ink-3); }
.hzn-meter {
width: 120px; height: 4px; border-radius: 2px; margin-top: 8px; margin-left: auto;
background: rgba(47,157,178,.16);
}
.hzn-meter span { display: block; height: 100%; border-radius: 2px; background: var(--up); }
/* ------- price rail ------- */
.hzn-rail { position: relative; height: 108px; margin: 34px 8px 4px; }
.hzn-rail-track {
position: absolute; left: 0; right: 0; top: 50%; height: 1px;
background: var(--grid);
}
.hzn-rail-risk {
position: absolute; top: calc(50% - 2px); height: 4px;
background: rgba(227,106,88,.16); border-radius: 2px;
}
.hzn-rail-progress {
position: absolute; top: calc(50% - 2px); height: 4px;
background: var(--up); border-radius: 2px;
}
.hzn-rail-mark { position: absolute; top: 0; bottom: 0; width: 0; }
.hzn-rail-tick {
position: absolute; top: calc(50% - 7px); left: -1px;
width: 2px; height: 14px; border-radius: 1px;
}
.hzn-rail-dot {
position: absolute; top: calc(50% - 6px); left: -6px;
width: 12px; height: 12px; border-radius: 50%;
background: var(--up); border: 2px solid var(--surface);
box-shadow: 0 0 0 1px var(--up), 0 0 14px rgba(110,201,219,.55);
}
.hzn-rail-ring {
position: absolute; top: calc(50% - 6px); left: -6px;
width: 12px; height: 12px; border-radius: 50%;
border: 2px solid var(--up); background: var(--void);
}
.hzn-rail-label {
position: absolute; top: calc(50% + 16px); left: 0; transform: translateX(-50%);
display: flex; flex-direction: column; align-items: center; gap: 1px;
white-space: nowrap;
}
.hzn-rail-label-top { top: auto; bottom: calc(50% + 16px); }
.hzn-rail-label em {
font-style: normal; font-family: 'IBM Plex Mono', monospace; font-size: 9.5px;
letter-spacing: .18em; text-transform: uppercase; color: var(--ink-3);
}
.hzn-rail-label b {
font-family: 'IBM Plex Mono', monospace; font-weight: 500; font-size: 13px;
color: var(--ink);
}
.hzn-rail-label i {
font-style: normal; font-family: 'IBM Plex Mono', monospace; font-size: 10.5px;
color: var(--ink-2);
}
.hzn-rail-now .hzn-rail-label b { color: var(--up-text); }
.hzn-focus-foot {
display: flex; justify-content: space-between; align-items: center;
gap: 16px; flex-wrap: wrap; margin-top: 18px;
border-top: 1px solid var(--grid); padding-top: 16px;
}
.hzn-note { font-size: 12.5px; color: var(--ink-3); }
.hzn-actions { display: flex; gap: 10px; }
.hzn-btn {
font-size: 13px; font-weight: 600; border-radius: 8px; padding: 7px 16px;
background: rgba(47,157,178,.16); color: var(--up-text);
border: 1px solid rgba(47,157,178,.35);
transition: background .15s ease, border-color .15s ease;
font-family: inherit; cursor: pointer;
}
.hzn-btn:hover { background: rgba(47,157,178,.26); border-color: rgba(110,201,219,.5); }
.hzn-btn-ghost { background: transparent; color: var(--ink-2); border-color: var(--panel-line); }
.hzn-btn-ghost:hover { color: var(--ink); background: rgba(226,231,245,.04); }
/* ------- KPI band ------- */
.hzn-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.hzn-tile {
background: var(--panel-glass); border: 1px solid var(--panel-line);
border-radius: 14px; padding: 18px 20px;
backdrop-filter: blur(14px);
}
.hzn-tile .hzn-stat-value { font-size: 26px; }
.hzn-tile-sub { font-size: 12px; color: var(--ink-3); margin-top: 4px; }
.hzn-tile-spark { display: flex; justify-content: space-between; align-items: flex-end; gap: 10px; }
/* ------- panels / columns ------- */
.hzn-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; align-items: start; }
.hzn-panel {
background: var(--panel-glass); border: 1px solid var(--panel-line);
border-radius: 14px; padding: 20px 22px;
backdrop-filter: blur(14px);
}
.hzn-panel-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.hzn-panel-hint { font-size: 11.5px; color: var(--ink-3); }
/* positions */
.hzn-poslist { list-style: none; display: flex; flex-direction: column; }
.hzn-poslist > li { border-top: 1px solid var(--grid); }
.hzn-poslist > li:first-child { border-top: 0; }
.hzn-pos {
display: grid; grid-template-columns: 92px 1fr 130px 64px 16px;
align-items: center; gap: 12px;
width: 100%; text-align: left; background: none; border: 0; color: inherit;
font: inherit; padding: 11px 4px; cursor: pointer;
transition: background .15s ease; border-radius: 8px;
}
.hzn-pos:hover { background: rgba(226,231,245,.035); }
.hzn-pos-id { display: flex; align-items: center; gap: 8px; }
.hzn-pos-id b { font-weight: 600; }
.hzn-pos-px { font-size: 12px; color: var(--ink-2); }
.hzn-pos-r { text-align: right; font-size: 13px; font-weight: 600; }
.hzn-caret { color: var(--ink-3); font-size: 10px; transition: transform .15s ease; }
.hzn-caret-open { transform: rotate(180deg); }
.hzn-rbar { position: relative; height: 10px; }
.hzn-rbar-zero {
position: absolute; left: 50%; top: -3px; bottom: -3px; width: 1px;
background: var(--panel-line);
}
.hzn-rbar-fill { position: absolute; top: 0; bottom: 0; }
.hzn-pos-detail {
display: flex; flex-direction: column; gap: 16px;
padding: 6px 8px 16px; font-size: 12.5px;
}
.hzn-pos-detail dl { display: grid; grid-template-columns: repeat(3, minmax(0, auto)); gap: 10px 34px; }
.hzn-pos-detail dt {
font-family: 'IBM Plex Mono', monospace; font-size: 9.5px; letter-spacing: .16em;
text-transform: uppercase; color: var(--ink-3);
}
.hzn-pos-detail dd { margin-top: 2px; color: var(--ink); }
.hzn-pos-detail dd i { font-style: normal; color: var(--ink-3); font-size: 11px; margin-left: 6px; }
.hzn-pos-chart { width: 100%; }
.hzn-tradechart { width: 100%; height: auto; display: block; margin-top: 8px; }
.hzn-lvl {
fill: var(--ink-3); font-family: 'IBM Plex Mono', monospace; font-size: 10px;
}
.hzn-lvl-up { fill: var(--up-text); }
.hzn-lvl-down { fill: var(--down-text); }
.hzn-lvl-ink { fill: var(--ink-2); }
/* radar */
.hzn-radar { list-style: none; display: flex; flex-direction: column; }
.hzn-radar li {
display: grid; grid-template-columns: 20px 118px 44px 1fr 52px minmax(120px, 1.2fr);
align-items: center; gap: 10px;
padding: 10px 4px; border-top: 1px solid var(--grid);
border-radius: 8px; transition: background .15s ease;
}
.hzn-radar li:first-child { border-top: 0; }
.hzn-radar li:hover { background: rgba(226,231,245,.035); }
.hzn-radar-dim { opacity: .62; }
.hzn-radar-rank { color: var(--ink-3); font-size: 11px; }
.hzn-radar-id b { font-weight: 600; display: block; }
.hzn-radar-id small { color: var(--ink-3); font-size: 10.5px; display: block; max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hzn-radar-mom { display: flex; align-items: center; gap: 8px; }
.hzn-radar-momtrack {
flex: 1; height: 4px; border-radius: 2px; background: rgba(226,231,245,.07);
overflow: hidden;
}
.hzn-radar-momtrack i { display: block; height: 100%; border-radius: 2px; }
.hzn-radar-mom small { color: var(--ink-3); font-size: 10px; }
.hzn-radar-rr { text-align: right; font-size: 12.5px; color: var(--ink-2); }
.hzn-radar-note { font-size: 11.5px; color: var(--ink-3); text-align: right; }
.hzn-radar-yes { color: var(--up-text); }
/* ------- chart ------- */
.hzn-chart {
background: var(--panel-glass); border: 1px solid var(--panel-line);
border-radius: 14px; padding: 20px 22px 12px;
backdrop-filter: blur(14px);
}
.hzn-chart-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
.hzn-chart-title { font-size: 13px; color: var(--ink-2); margin-top: 5px; }
.hzn-legend { display: flex; gap: 16px; font-size: 12px; color: var(--ink-2); }
.hzn-legend i {
display: inline-block; width: 9px; height: 9px; border-radius: 50%;
margin-right: 6px; vertical-align: -1px;
}
.hzn-chart-svg { width: 100%; height: auto; margin-top: 10px; display: block; }
.hzn-axis { fill: var(--ink-3); font-family: 'IBM Plex Mono', monospace; font-size: 10px; }
.hzn-endlabel { fill: var(--ink); font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 600; }
.hzn-endlabel-dim { fill: var(--ink-2); font-weight: 400; }
.hzn-chart-tip {
font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--ink-3);
padding: 8px 2px 4px; min-height: 28px;
}
.hzn-chart-tip b { color: var(--ink); font-weight: 500; }
/* ------- radar fingerprint ------- */
.hzn-radar-wrap { position: relative; display: inline-block; }
.hzn-radar-tip {
position: absolute; transform: translate(-50%, calc(-100% - 12px));
background: rgba(21, 24, 35, 0.96); border: 1px solid var(--panel-line);
border-radius: 8px; padding: 6px 10px; white-space: nowrap;
pointer-events: none; z-index: 5; box-shadow: 0 6px 24px rgba(0,0,0,.45);
font-size: 12px;
}
.hzn-radar-tip b { display: block; color: var(--ink); font-weight: 600; }
.hzn-radar-tip span { display: block; color: var(--up-text); font-size: 11px; margin-top: 1px; }
.hzn-radar-tip i { display: block; font-style: normal; color: var(--ink-3); font-size: 10px; margin-top: 1px; }
.hzn-radar-axis {
fill: var(--ink-3); font-family: 'IBM Plex Mono', monospace; font-size: 9.5px;
letter-spacing: .04em;
}
.hzn-radar-axisval { fill: var(--ink-2); font-weight: 600; }
.hzn-tk-radar { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.hzn-cmp-row {
display: flex; gap: 14px; align-items: center; flex-wrap: wrap;
border-bottom: 1px solid var(--grid); padding-bottom: 12px; margin-bottom: 10px;
}
.hzn-cmp-fig { text-align: center; }
.hzn-cmp-fig figcaption b { display: block; font-size: 13px; font-weight: 600; margin-top: -2px; }
.hzn-cmp-fig figcaption span {
font-family: 'IBM Plex Mono', monospace; font-size: 9px; letter-spacing: .12em;
text-transform: uppercase; color: var(--ink-3);
}
.hzn-cmp-fig .hzn-cmp-pick { color: var(--ember); }
.hzn-cmp-axes {
font-family: 'IBM Plex Mono', monospace; font-size: 10px; line-height: 1.7;
color: var(--ink-3); margin-left: auto; text-align: right; max-width: 220px;
}
/* ------- ticker drill-down ------- */
.hzn-ticker-top { display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.hzn-tk-title { display: flex; align-items: baseline; gap: 10px; margin-top: 8px; }
.hzn-tk-title b { font-family: 'Space Grotesk', sans-serif; font-size: 30px; font-weight: 700; }
.hzn-tk-title span { color: var(--ink-2); font-size: 13px; }
.hzn-tk-price { display: flex; align-items: baseline; gap: 10px; margin-top: 4px; }
.hzn-tk-price .num:first-child { font-size: 18px; color: var(--ink); }
.hzn-tk-price b { font-size: 13px; }
.hzn-tk-price i { font-style: normal; font-size: 11.5px; color: var(--ink-3); }
.hzn-tk-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.hzn-tk-bars {
display: flex; justify-content: space-between; align-items: center; gap: 14px;
flex-wrap: wrap; margin-top: 16px; border-bottom: 1px solid var(--grid); padding-bottom: 10px;
}
.hzn-tk-tabs { display: flex; gap: 18px; font-size: 13px; color: var(--ink-2); }
.hzn-tk-tabs a:hover { color: var(--ink); }
.hzn-tk-tab-on { color: var(--ink); border-bottom: 1px solid var(--up); padding-bottom: 2px; }
.hzn-tk-ranges { display: flex; gap: 4px; }
.hzn-tk-ranges button {
font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; color: var(--ink-3);
background: none; border: 1px solid transparent; border-radius: 6px;
padding: 3px 8px; cursor: pointer;
}
.hzn-tk-ranges button:hover { color: var(--ink-2); }
.hzn-tk-ranges .hzn-tk-range-on {
color: var(--up-text); border-color: rgba(47,157,178,.35); background: rgba(47,157,178,.12);
}
.hzn-fresh {
display: flex; gap: 16px; flex-wrap: wrap; align-items: center;
font-family: 'IBM Plex Mono', monospace; font-size: 10px; letter-spacing: .06em;
color: var(--ink-3); border-top: 1px solid var(--grid); padding-top: 10px; margin-top: 4px;
}
.hzn-fresh span { display: inline-flex; align-items: center; gap: 6px; }
.hzn-fresh i {
width: 5px; height: 5px; border-radius: 50%; background: var(--up); display: inline-block;
}
.hzn-fresh a { color: var(--ink-2); margin-left: auto; }
.hzn-fresh a:hover { color: var(--ink); }
/* ------- watchlist ------- */
.hzn-watch-chips { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.hzn-wchip {
display: inline-flex; align-items: baseline; gap: 8px;
border: 1px solid var(--panel-line); border-radius: 99px;
padding: 6px 14px; font-size: 13px;
transition: border-color .15s ease, background .15s ease;
}
.hzn-wchip:hover { border-color: rgba(110,201,219,.4); background: rgba(226,231,245,.03); }
.hzn-wchip b { font-weight: 600; }
.hzn-wchip span { font-size: 11.5px; color: var(--ink-2); }
/* ------- footer ------- */
.hzn-foot {
font-family: 'IBM Plex Mono', monospace; font-size: 10px;
letter-spacing: .3em; text-transform: uppercase; color: var(--ink-3);
text-align: center; margin-top: 10px;
}
/* ------- responsive ------- */
@media (max-width: 980px) {
.hzn-kpis { grid-template-columns: repeat(2, 1fr); }
.hzn-cols { grid-template-columns: 1fr; }
.hzn-radar li { grid-template-columns: 20px 110px 44px 1fr 48px; }
.hzn-radar-note { grid-column: 2 / -1; text-align: left; padding-bottom: 2px; }
}
@media (max-width: 640px) {
.hzn-page { padding: 20px 16px 24vh; }
.hzn-kpis { grid-template-columns: 1fr; }
.hzn-focus-stats { text-align: left; }
.hzn-meter { margin-left: 0; }
.hzn-pos { grid-template-columns: 86px 1fr 60px 14px; }
.hzn-pos-px { display: none; }
}
/* ------- motion ------- */
@media (prefers-reduced-motion: no-preference) {
.hzn-page { animation: hzn-rise .5s ease-out both; }
@keyframes hzn-rise {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
}
`;