From 5e106fe40a073f248a70317604fe4aead2c4158e Mon Sep 17 00:00:00 2001 From: aurph Date: Thu, 2 Jul 2026 14:08:20 -0400 Subject: [PATCH 01/26] Lake 1: design token layer - surfaces, semantic + categorical + data-quality colors, type scale, chart theme Categorical palette CVD-validated (min adjacent dE 13.9 both surfaces). Tabular figures on all mono/table numerals. --- client/src/index.css | 78 +++++++++++++++++++++ client/src/lib/chart-theme.ts | 99 +++++++++++++++++++++++++++ client/src/lib/tokens.ts | 125 ++++++++++++++++++++++++++++++++++ tailwind.config.ts | 61 +++++++++++++++++ 4 files changed, 363 insertions(+) create mode 100644 client/src/lib/chart-theme.ts create mode 100644 client/src/lib/tokens.ts diff --git a/client/src/index.css b/client/src/index.css index c8ff07a..86bfb1e 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -95,6 +95,76 @@ --destructive-border: hsl(from hsl(var(--destructive)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); } +/* + * GridTilt data tokens (Lake 1) + * Single source of truth for every color, size, and chart value in the + * dashboard. TS mirror for chart code: client/src/lib/tokens.ts - keep in sync. + * The Swiss homepage (/) has its own isolated contract in styles/anchor.css. + */ +:root { + /* Surface scale - 4 elevations, dark only */ + --surface-sunken: #0A0A08; + --surface-base: #0E0E0C; + --surface-raised: #1A1917; + --surface-overlay: #26241F; + --border-subtle: rgba(255, 255, 255, 0.06); + --border-strong: rgba(255, 255, 255, 0.14); + + /* Brand */ + --brand: #F07800; + --brand-2: #F0A500; + --brand-glow: rgba(240, 120, 0, 0.12); + --brand-wash: rgba(240, 120, 0, 0.05); + + /* Ink scale (text on dark surfaces) */ + --ink: #F2F1ED; + --ink-secondary: #B0AEA6; + --ink-muted: #7A7871; + --ink-faint: #55534E; + + /* Semantic - state, never series identity */ + --positive: #4ade80; + --positive-deep: #22c55e; + --negative: #f87171; + --negative-deep: #ef4444; + --warning: #eab308; + --critical: #dc2626; + --info: #4dabf7; + + /* Data quality - sourced vs estimated vs synthetic fill */ + --estimate: #d4a843; + --dq-estimated-opacity: 0.55; + --dq-synthetic-opacity: 0.35; + + /* Categorical series - fixed order, CVD-validated (min adjacent dE 13.9 + on both surfaces). Assign in order, never cycle; >4 series need direct + labels. Do not reorder: the order IS the colorblind-safety mechanism. */ + --series-1: #3987e5; /* blue */ + --series-2: #c98500; /* amber */ + --series-3: #199e70; /* teal */ + --series-4: #9085e9; /* violet */ + --series-5: #d55181; /* magenta */ + --series-6: #1f9fb5; /* cyan */ + --series-7: #d95926; /* rust */ + --series-8: #3d9e3d; /* green */ + --series-9: #bd6bce; /* pink */ + --series-10: #b07d3f; /* brown */ + + /* Chart chrome */ + --chart-axis: #55534E; + --chart-tick: #7A7871; + --chart-grid: rgba(255, 255, 255, 0.05); + --chart-crosshair: rgba(255, 255, 255, 0.25); + --chart-ref-line: rgba(255, 255, 255, 0.18); + + /* Focus + motion */ + --focus-ring-color: #F0A500; + --focus-ring-width: 2px; + --duration-fast: 120ms; + --duration-base: 200ms; + --duration-slow: 350ms; +} + /* Force dark mode always */ html { color-scheme: dark; @@ -108,6 +178,14 @@ html { body { @apply font-sans antialiased bg-background text-foreground; } + + /* Tabular figures on all data type so numbers never jitter on refresh */ + .font-mono, + code, + td, + th { + font-variant-numeric: tabular-nums; + } } /* Custom scrollbar */ diff --git a/client/src/lib/chart-theme.ts b/client/src/lib/chart-theme.ts new file mode 100644 index 0000000..67c9b68 --- /dev/null +++ b/client/src/lib/chart-theme.ts @@ -0,0 +1,99 @@ +/** + * GridTilt chart theme (Lake 1) - one theme object consumed by every chart. + * Recharts charts spread the prop bundles; d3/SVG/visx charts read the raw + * values. No chart should hardcode an axis color, tick size, or tooltip + * style after this file exists. + */ +import type { CSSProperties } from "react"; +import { BORDER, CHART_CHROME, FONT, INK, SURFACE } from "./tokens"; + +export const chartTheme = { + axis: { + stroke: CHART_CHROME.axis, + tickFill: CHART_CHROME.tick, + fontSize: 10, + fontFamily: FONT.mono, + }, + grid: { + stroke: CHART_CHROME.grid, + strokeDasharray: "3 3", + }, + crosshair: { + stroke: CHART_CHROME.crosshair, + strokeWidth: 1, + strokeDasharray: "3 3", + }, + refLine: { + stroke: CHART_CHROME.refLine, + strokeDasharray: "4 4", + }, + tooltip: { + background: SURFACE.overlay, + border: BORDER.strong, + labelColor: INK.secondary, + valueColor: INK.primary, + fontSize: 11, + fontFamily: FONT.mono, + radius: 4, + }, + label: { + fontSize: 11, + fontFamily: FONT.mono, + fill: INK.secondary, + }, + line: { + strokeWidth: 2, + dot: false, + activeDotRadius: 3, + }, +} as const; + +/** Spread into Recharts /: {...axisProps} */ +export const axisProps = { + stroke: chartTheme.axis.stroke, + tick: { + fontSize: chartTheme.axis.fontSize, + fill: chartTheme.axis.tickFill, + fontFamily: chartTheme.axis.fontFamily, + }, + tickLine: false as const, + axisLine: { stroke: chartTheme.axis.stroke }, +}; + +/** Spread into Recharts : {...gridProps} */ +export const gridProps = { + stroke: chartTheme.grid.stroke, + strokeDasharray: chartTheme.grid.strokeDasharray, + vertical: false as const, +}; + +/** Pass to Recharts */ +export const tooltipContentStyle: CSSProperties = { + background: chartTheme.tooltip.background, + border: `1px solid ${chartTheme.tooltip.border}`, + borderRadius: chartTheme.tooltip.radius, + fontSize: chartTheme.tooltip.fontSize, + fontFamily: chartTheme.tooltip.fontFamily, + padding: "8px 10px", +}; + +export const tooltipLabelStyle: CSSProperties = { + color: chartTheme.tooltip.labelColor, + fontSize: chartTheme.tooltip.fontSize, + fontFamily: chartTheme.tooltip.fontFamily, + marginBottom: 4, +}; + +export const tooltipItemStyle: CSSProperties = { + color: chartTheme.tooltip.valueColor, + fontSize: chartTheme.tooltip.fontSize, + fontFamily: chartTheme.tooltip.fontFamily, + padding: 0, +}; + +/** Pass to Recharts for the crosshair */ +export const tooltipCursor = { + stroke: chartTheme.crosshair.stroke, + strokeWidth: chartTheme.crosshair.strokeWidth, + strokeDasharray: chartTheme.crosshair.strokeDasharray, +}; diff --git a/client/src/lib/tokens.ts b/client/src/lib/tokens.ts new file mode 100644 index 0000000..9620156 --- /dev/null +++ b/client/src/lib/tokens.ts @@ -0,0 +1,125 @@ +/** + * GridTilt data tokens (Lake 1) - TS mirror of the :root vars in index.css. + * Chart code (Recharts/d3/SVG props) needs literal values, so this file is + * the source of truth for anything rendered outside the CSS cascade. + * Keep in sync with index.css; Lake 8 adds a test asserting the sync. + */ + +export const SURFACE = { + sunken: "#0A0A08", + base: "#0E0E0C", + raised: "#1A1917", + overlay: "#26241F", +} as const; + +export const BORDER = { + subtle: "rgba(255, 255, 255, 0.06)", + strong: "rgba(255, 255, 255, 0.14)", +} as const; + +export const BRAND = { + primary: "#F07800", + secondary: "#F0A500", + glow: "rgba(240, 120, 0, 0.12)", + wash: "rgba(240, 120, 0, 0.05)", +} as const; + +export const INK = { + primary: "#F2F1ED", + secondary: "#B0AEA6", + muted: "#7A7871", + faint: "#55534E", +} as const; + +/** State colors. Never use these for series identity. */ +export const SEMANTIC = { + positive: "#4ade80", + positiveDeep: "#22c55e", + negative: "#f87171", + negativeDeep: "#ef4444", + warning: "#eab308", + critical: "#dc2626", + info: "#4dabf7", +} as const; + +/** + * Data-quality treatment: sourced points render solid at full opacity; + * estimated values keep the series hue at reduced opacity; synthetic fill + * spans (interpolation between anchors) render dashed at low opacity. + */ +export const DATA_QUALITY = { + estimateFlag: "#d4a843", + estimatedOpacity: 0.55, + syntheticOpacity: 0.35, + syntheticDash: "4 3", +} as const; + +/** + * Categorical palette - fixed order, CVD-validated on both dark surfaces + * (min adjacent deltaE 13.9 under protanopia/deuteranopia, all slots >= 3:1 + * contrast). Assign slots in order, never cycle. The ORDER is the + * colorblind-safety mechanism: do not reorder or insert. + * At >4 visible series, direct labels are mandatory (color alone is not + * enough in scatter/treemap contexts where any two slots can be adjacent). + */ +export const SERIES = [ + "#3987e5", // 1 blue + "#c98500", // 2 amber + "#199e70", // 3 teal + "#9085e9", // 4 violet + "#d55181", // 5 magenta + "#1f9fb5", // 6 cyan + "#d95926", // 7 rust + "#3d9e3d", // 8 green + "#bd6bce", // 9 pink + "#b07d3f", // 10 brown +] as const; + +/** + * Stable category -> color mapping. Same category = same color everywhere + * in the app (Tilt Overview mover tags, The Stack layers, Power Map, + * Compute Frontier, TheTrade). Categories that never co-occur in one chart + * may share a slot (solar/power); co-occurring ones never do. + */ +export const CATEGORY_COLORS: Record = { + // Sectors / mover tags + compute: SERIES[0], // blue + datacenters: SERIES[2], // teal + construction: SERIES[9], // brown + power: SERIES[1], // amber + utilities: SERIES[5], // cyan + uranium: SERIES[4], // magenta + // Energy sources (TheTrade + Compute Frontier co-occur: must be distinct) + nuclear: SERIES[3], // violet - matches existing purple convention + gas: SERIES[6], // rust + renewables: SERIES[7], // green + grid: SERIES[5], // cyan + solar: SERIES[1], // amber (never co-charts with power) + wind: SERIES[0], // blue (never co-charts with compute) + hydro: SERIES[2], // teal + storage: SERIES[8], // pink + coal: INK.faint, +}; + +/** + * Facility/project status - state, not identity, so it draws on semantic + * steps. Used by Power Map markers and Compute Frontier status charts. + */ +export const STATUS_COLORS = { + operational: SEMANTIC.positiveDeep, + construction: SEMANTIC.warning, + announced: INK.muted, +} as const; + +export const CHART_CHROME = { + axis: "#55534E", + tick: "#7A7871", + grid: "rgba(255, 255, 255, 0.05)", + crosshair: "rgba(255, 255, 255, 0.25)", + refLine: "rgba(255, 255, 255, 0.18)", +} as const; + +export const FONT = { + mono: '"JetBrains Mono", monospace', + sans: "Inter, sans-serif", +} as const; diff --git a/tailwind.config.ts b/tailwind.config.ts index 5daa35e..a5879f5 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -87,6 +87,67 @@ export default { "green-low": "#22c55e", "yellow-mid": "#eab308", "red-high": "#ef4444", + // GridTilt data tokens (Lake 1) - values live in index.css :root + surface: { + sunken: "var(--surface-sunken)", + DEFAULT: "var(--surface-base)", + base: "var(--surface-base)", + raised: "var(--surface-raised)", + overlay: "var(--surface-overlay)", + }, + brand: { + DEFAULT: "var(--brand)", + "2": "var(--brand-2)", + }, + ink: { + DEFAULT: "var(--ink)", + secondary: "var(--ink-secondary)", + muted: "var(--ink-muted)", + faint: "var(--ink-faint)", + }, + positive: { + DEFAULT: "var(--positive)", + deep: "var(--positive-deep)", + }, + negative: { + DEFAULT: "var(--negative)", + deep: "var(--negative-deep)", + }, + warning: "var(--warning)", + critical: "var(--critical)", + info: "var(--info)", + estimate: "var(--estimate)", + series: { + "1": "var(--series-1)", + "2": "var(--series-2)", + "3": "var(--series-3)", + "4": "var(--series-4)", + "5": "var(--series-5)", + "6": "var(--series-6)", + "7": "var(--series-7)", + "8": "var(--series-8)", + "9": "var(--series-9)", + "10": "var(--series-10)", + }, + }, + borderColor: { + subtle: "var(--border-subtle)", + strong: "var(--border-strong)", + }, + fontSize: { + // Data type scale - replaces the ad-hoc text-[NNpx] values. + // 12/14/16px stay on the default xs/sm/base steps. + "8": ["0.5rem", { lineHeight: "0.75rem" }], + "9": ["0.5625rem", { lineHeight: "0.75rem" }], + "10": ["0.625rem", { lineHeight: "0.875rem" }], + "11": ["0.6875rem", { lineHeight: "1rem" }], + "13": ["0.8125rem", { lineHeight: "1.25rem" }], + "15": ["0.9375rem", { lineHeight: "1.375rem" }], + }, + transitionDuration: { + fast: "var(--duration-fast)", + base: "var(--duration-base)", + slow: "var(--duration-slow)", }, fontFamily: { sans: ["Inter", "sans-serif"], From 61837c602a16d48c74b6d60edf272f987a2024db Mon Sep 17 00:00:00 2001 From: aurph Date: Thu, 2 Jul 2026 19:17:15 -0400 Subject: [PATCH 02/26] Lake 1: migrate CatalystTracker, SupplyChain, TheStack, Compute Frontier to tokens Stack layers: 10 thesis layers own the 10 categorical slots, periphery layers (mining, crypto operators, benchmarks) take neutral ink tiers. Catalyst categories and supply-chain stages on distinct series slots instead of four near-identical oranges. --- client/src/data/catalyst-config.ts | 28 +++-- client/src/data/supply-chain-config.ts | 14 ++- client/src/index.css | 60 ++++----- client/src/lib/tokens.ts | 4 +- client/src/pages/CatalystTracker.tsx | 153 +++++++++++------------ client/src/pages/SupplyChain.tsx | 85 ++++++------- client/src/pages/TheStack.tsx | 83 +++++++------ client/src/pages/compute-frontier.tsx | 164 ++++++++++++------------- 8 files changed, 301 insertions(+), 290 deletions(-) diff --git a/client/src/data/catalyst-config.ts b/client/src/data/catalyst-config.ts index 15ca683..1d799b1 100644 --- a/client/src/data/catalyst-config.ts +++ b/client/src/data/catalyst-config.ts @@ -1,11 +1,16 @@ +import { SERIES } from '@/lib/tokens'; + export type CatalystCategory = 'Regulatory' | 'Policy' | 'Infrastructure' | 'Market' | 'Industry'; +// Categories not in tokens.ts CATEGORY_COLORS: assigned SERIES slots in order +// of appearance so the categories are actually distinguishable (the old map +// put them all on near-identical oranges). export const catalystCategoryColors: Record = { - Regulatory: '#F0A500', - Policy: '#D4A843', - Infrastructure: '#C87533', - Market: '#F07800', - Industry: '#F07800', + Regulatory: SERIES[0], // series slot 1 (blue) + Policy: SERIES[1], // series slot 2 (amber) + Infrastructure: SERIES[2], // series slot 3 (teal) + Market: SERIES[3], // series slot 4 (violet) + Industry: SERIES[4], // series slot 5 (magenta) }; export interface ManualCatalyst { @@ -126,10 +131,13 @@ export const SUPPLY_CHAIN_STAGE_MAP: Record = { IREN: 'End Use', CLSK: 'End Use', MARA: 'End Use', }; +// Supply-chain stages are not in tokens.ts CATEGORY_COLORS: SERIES slots +// continue in order of appearance (slots 6-10) so stage dots and catalyst +// category dots stay distinguishable when they co-occur on the calendar. export const STAGE_COLORS: Record = { - 'Raw Materials': '#C87533', - 'Generation': '#F07800', - 'Transmission': '#D4A843', - 'Distribution': '#B8860B', - 'End Use': '#F0A500', + 'Raw Materials': SERIES[5], // series slot 6 (cyan) + 'Generation': SERIES[6], // series slot 7 (rust) + 'Transmission': SERIES[7], // series slot 8 (green) + 'Distribution': SERIES[8], // series slot 9 (pink) + 'End Use': SERIES[9], // series slot 10 (brown) }; diff --git a/client/src/data/supply-chain-config.ts b/client/src/data/supply-chain-config.ts index c081a99..34c612b 100644 --- a/client/src/data/supply-chain-config.ts +++ b/client/src/data/supply-chain-config.ts @@ -1,3 +1,5 @@ +import { BRAND, SERIES } from "@/lib/tokens"; + export interface SupplyNode { id: string; name: string; @@ -15,12 +17,14 @@ export interface SupplyLink { label?: string; } +// Supply-chain stages are not in CATEGORY_COLORS; per the migration map, +// brand hexes go to BRAND and the rest take SERIES slots in order of appearance. export const STAGE_COLORS: Record = { - 'raw-materials': '#C87533', - 'generation': '#F07800', - 'transmission': '#D4A843', - 'distribution': '#B8860B', - 'end-use': '#F0A500', + 'raw-materials': SERIES[0], // series slot 1 + 'generation': BRAND.primary, + 'transmission': SERIES[1], // series slot 2 + 'distribution': SERIES[2], // series slot 3 + 'end-use': BRAND.secondary, }; export const STAGE_LABELS: { id: string; name: string; index: number }[] = [ diff --git a/client/src/index.css b/client/src/index.css index 86bfb1e..a77a2aa 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -370,8 +370,8 @@ html { flex-wrap: wrap; gap: 8px; padding: 8px 12px; - background: #111110; - border: 1px solid rgba(255, 255, 255, 0.06); + background: var(--surface-base); + border: 1px solid var(--border-subtle); border-radius: 3px; margin-bottom: 16px; } @@ -384,15 +384,15 @@ html { } .sc-topbar-sep { - color: #333; + color: var(--ink-faint); font-size: 11px; } .sc-graph-container { position: relative; width: 100%; - background: #0E0E0C; - border: 1px solid rgba(255, 255, 255, 0.06); + background: var(--surface-base); + border: 1px solid var(--border-subtle); border-radius: 3px; overflow: hidden; } @@ -423,7 +423,7 @@ html { font-size: 10px; font-weight: 600; letter-spacing: 2px; - fill: #F07800; + fill: var(--brand); } .sc-node-label { @@ -440,7 +440,7 @@ html { .sc-link-label { font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 8px; - fill: #888; + fill: var(--ink-muted); } .sc-stage-pill-text { @@ -457,12 +457,12 @@ html { gap: 14px; padding: 8px 12px; margin-top: 12px; - background: #0E0E0C; - border: 1px solid rgba(255, 255, 255, 0.05); + background: var(--surface-base); + border: 1px solid var(--border-subtle); border-radius: 3px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 10px; - color: #777; + color: var(--ink-faint); } .sc-legend-swatch { display: inline-block; @@ -474,13 +474,13 @@ html { } .sc-legend-hint { margin-left: auto; - color: #555; + color: var(--ink-faint); letter-spacing: 0.5px; } .sc-view-toggle { display: inline-flex; - border: 1px solid rgba(255, 255, 255, 0.08); + border: 1px solid var(--border-subtle); border-radius: 3px; overflow: hidden; } @@ -490,17 +490,17 @@ html { letter-spacing: 1px; padding: 3px 10px; background: transparent; - color: #777; + color: var(--ink-faint); border: none; cursor: pointer; transition: color 0.15s, background 0.15s; } .sc-view-btn:hover { - color: #fff; + color: var(--ink); } .sc-view-btn-active { - background: rgba(240, 120, 0, 0.15); - color: #F07800; + background: var(--brand-glow); + color: var(--brand); } .sc-node-hidden { @@ -534,8 +534,8 @@ html { width: 100%; max-width: 900px; margin: 16px auto 0; - background: #141412; - border: 1px solid rgba(240, 120, 0, 0.12); + background: var(--surface-base); + border: 1px solid var(--brand-glow); border-radius: 4px; padding: 24px; animation: scSlideDown 0.3s ease-out; @@ -548,7 +548,7 @@ html { .sc-divider { height: 1px; - background: rgba(255, 255, 255, 0.06); + background: var(--border-subtle); } .sc-section-label { @@ -556,7 +556,7 @@ html { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; - color: #444; + color: var(--ink-faint); margin-bottom: 8px; } @@ -564,9 +564,9 @@ html { display: inline-block; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 11px; - color: #ccc; - background: #1A1917; - border: 1px solid rgba(255, 255, 255, 0.05); + color: var(--ink-secondary); + background: var(--surface-raised); + border: 1px solid var(--border-subtle); border-radius: 3px; padding: 3px 8px; } @@ -575,12 +575,12 @@ html { transition: border-color 0.2s, color 0.2s; } .sc-flow-tag-clickable:hover { - border-color: rgba(240, 120, 0, 0.4); - color: #F07800; + border-color: color-mix(in srgb, var(--brand) 40%, transparent); + color: var(--brand); } .sc-stock-table { - border: 1px solid rgba(255, 255, 255, 0.06); + border: 1px solid var(--border-subtle); border-radius: 2px; overflow: hidden; } @@ -590,12 +590,12 @@ html { grid-template-columns: 70px 1fr 90px 80px; gap: 0; padding: 6px 10px; - background: #0E0E0C; + background: var(--surface-base); font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 9px; font-weight: 600; letter-spacing: 0.08em; - color: #444; + color: var(--ink-faint); } .sc-stock-row { @@ -605,12 +605,12 @@ html { padding: 7px 10px; font-size: 11px; cursor: pointer; - border-top: 1px solid rgba(255, 255, 255, 0.03); + border-top: 1px solid var(--border-subtle); transition: background 0.12s; } .sc-stock-row:hover { - background: rgba(240, 120, 0, 0.04); + background: var(--brand-wash); } .sc-stock-col-ticker { text-align: left; } diff --git a/client/src/lib/tokens.ts b/client/src/lib/tokens.ts index 9620156..151511b 100644 --- a/client/src/lib/tokens.ts +++ b/client/src/lib/tokens.ts @@ -87,7 +87,7 @@ export const CATEGORY_COLORS: Record = { datacenters: SERIES[2], // teal construction: SERIES[9], // brown power: SERIES[1], // amber - utilities: SERIES[5], // cyan + utilities: SERIES[8], // pink (grid owns cyan; both appear on The Stack) uranium: SERIES[4], // magenta // Energy sources (TheTrade + Compute Frontier co-occur: must be distinct) nuclear: SERIES[3], // violet - matches existing purple convention @@ -97,7 +97,7 @@ export const CATEGORY_COLORS: Record = { solar: SERIES[1], // amber (never co-charts with power) wind: SERIES[0], // blue (never co-charts with compute) hydro: SERIES[2], // teal - storage: SERIES[8], // pink + storage: SERIES[4], // magenta (never co-charts with uranium) coal: INK.faint, }; diff --git a/client/src/pages/CatalystTracker.tsx b/client/src/pages/CatalystTracker.tsx index 9a45992..0f6130c 100644 --- a/client/src/pages/CatalystTracker.tsx +++ b/client/src/pages/CatalystTracker.tsx @@ -11,6 +11,7 @@ import { SUPPLY_CHAIN_STAGE_MAP, type CatalystCategory, } from "@/data/catalyst-config"; +import { BRAND, INK, SURFACE, BORDER } from "@/lib/tokens"; interface EarningsItem { id: string; @@ -68,7 +69,7 @@ function formatDateFull(dateStr: string): string { function getStageColor(ticker: string): string { const stage = SUPPLY_CHAIN_STAGE_MAP[ticker]; - return stage ? (STAGE_COLORS[stage] || "#888") : "#888"; + return stage ? (STAGE_COLORS[stage] || INK.muted) : INK.muted; } function CalendarGrid({ @@ -116,24 +117,24 @@ function CalendarGrid({ return (
- - {monthName} -
-
+
{["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((d) => ( -
+
{d}
))} {cells.map((day, i) => { if (day === null) { - return
; + return
; } const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; const dayItems = itemsByDate[dateStr] || []; @@ -146,25 +147,25 @@ function CalendarGrid({ key={dateStr} className="cursor-pointer transition-all" style={{ - background: isSelected ? "#1E1D1A" : isToday ? "#1C1B18" : "#161614", + background: SURFACE.raised, minHeight: 56, padding: "4px 6px", - borderLeft: isToday ? "2px solid #F07800" : isSelected ? "2px solid rgba(255,255,255,0.2)" : "2px solid transparent", + borderLeft: isToday ? `2px solid ${BRAND.primary}` : isSelected ? `2px solid ${BORDER.strong}` : "2px solid transparent", opacity: isWeekend && dayItems.length === 0 ? 0.6 : 1, }} onClick={() => onDateSelect(isSelected ? null : dateStr)} data-testid={`calendar-day-${dateStr}`} > -
+
{day}
{dayItems.map((item, j) => { - let color = "#888"; + let color: string = INK.muted; if (item.type === "earnings") { color = (item as EarningsItem).stageColor || getStageColor((item as EarningsItem).ticker); } else { - color = catalystCategoryColors[(item as CatalystItem).category] || "#888"; + color = catalystCategoryColors[(item as CatalystItem).category] || INK.muted; } return (
{dayItems.length > 0 && ( -
+
{dayItems.length === 1 ? (dayItems[0].type === "earnings" ? (dayItems[0] as EarningsItem).ticker : "Event") : `${dayItems.length} events`} @@ -190,7 +191,7 @@ function CalendarGrid({ {selectedItems.length > 0 && (
-
+
{formatDateFull(selectedDate!)}
{selectedItems.map((item) => ( @@ -210,24 +211,24 @@ function DayDetailCard({ item }: { item: MergedItem }) { return (
- {e.ticker} - {e.company} + {e.ticker} + {e.company}
{e.time}
-
+
{e.stage} @@ -235,7 +236,7 @@ function DayDetailCard({ item }: { item: MergedItem }) { {e.quarter && {e.quarter}}
{node.keyMetric && ( -
+
{node.keyMetric.label}: {node.keyMetric.value}
)}
-

{node.description}

+

{node.description}

{upstreamNodes.length > 0 && (
-
RECEIVES FROM
+
RECEIVES FROM
{upstreamNodes.map((u, i) => ( {u.name} - {u.label && ({u.label})} + {u.label && ({u.label})} ))}
@@ -721,7 +722,7 @@ function DetailPanel({ )} {downstreamNodes.length > 0 && (
-
FEEDS INTO
+
FEEDS INTO
{downstreamNodes.map((d, i) => ( {d.name} - {d.label && ({d.label})} + {d.label && ({d.label})} ))}
@@ -753,10 +754,10 @@ function DetailPanel({ const price = stock?.price; const hasLiveChg = typeof chg === "number" && Number.isFinite(chg); const chgColor = !hasLiveChg - ? "text-[#555]" + ? "text-ink-faint" : chg! >= 0 - ? "text-[#22C55E]" - : "text-[#EF4444]"; + ? "text-positive" + : "text-negative"; return (
onNavigate(`/stock/${c.ticker}`)} data-testid={`company-${c.ticker}`} > - {c.ticker} - {c.name} - {price ? `$${price.toFixed(2)}` : "--"} + {c.ticker} + {c.name} + {price ? `$${price.toFixed(2)}` : "--"} {hasLiveChg ? `${chg! >= 0 ? "+" : ""}${chg!.toFixed(2)}%` : "--"} {stock?.stale && ( @@ -777,7 +778,7 @@ function DetailPanel({ onClick={(e) => e.stopPropagation()} data-testid={`stale-indicator-${c.ticker}`} > - + @@ -855,19 +856,19 @@ export default function SupplyChain() {
setActiveNode(null)}>
- SUPPLY CHAIN + SUPPLY CHAIN | - AI Power Infrastructure + AI Power Infrastructure
- NODES - {supplyNodes.length} + NODES + {supplyNodes.length} | - CONNECTIONS - {supplyLinks.length} + CONNECTIONS + {supplyLinks.length} | - SECURITIES - {totalCompanies} + SECURITIES + {totalCompanies} |
e.stopPropagation()}>
- +
@@ -139,20 +141,20 @@ function StockCard({ stock, showPower, showVsSP500 }: { stock: StockData; showPo

Rev Growth YoY

-

0 ? "text-green-400" : stock.revenueGrowth ? "text-red-400" : "text-muted-foreground"}`}> +

0 ? "text-positive" : stock.revenueGrowth ? "text-negative" : "text-muted-foreground"}`}> {stock.revenueGrowth ? `${stock.revenueGrowth > 0 ? "+" : ""}${stock.revenueGrowth.toFixed(1)}%` : "N/A"}

{showPower && stock.powerMW && (

Power / Facility

-

{stock.powerMW} MW avg

+

{stock.powerMW} MW avg

)} {showVsSP500 && stock.vs_sp500 !== undefined && (

vs S&P 500 (1Y)

-

0 ? "text-green-400" : "text-red-400"}`}> +

0 ? "text-positive" : "text-negative"}`}> {stock.vs_sp500 > 0 ? : } {stock.vs_sp500 > 0 ? "+" : ""}{stock.vs_sp500.toFixed(1)}%

@@ -268,7 +270,7 @@ export default function TheStack() { key: "compute", title: "Compute Layer", icon: Cpu, - color: "#94a3b8", + color: CATEGORY_COLORS.compute, description: "AI chips, hyperscalers, and the foundries powering model training.", tooltip: "NVIDIA's H100/B200 GPUs power virtually every major AI training cluster. TSMC manufactures all advanced AI chips. Hyperscalers (MSFT, GOOGL, META, AMZN) are both the largest compute consumers and primary drivers of data center power demand.", }, @@ -276,7 +278,7 @@ export default function TheStack() { key: "nuclear", title: "Nuclear Power", icon: Zap, - color: "#F0A500", + color: CATEGORY_COLORS.nuclear, description: "Nuclear operators, SMR developers, and advanced reactor companies.", tooltip: "AI requires uninterruptible clean baseload. Microsoft restarted Three Mile Island. Amazon co-located with Talen's Susquehanna plant. Oklo has a 14 GW DC customer pipeline. BWXT is the sole US naval reactor manufacturer.", }, @@ -284,7 +286,7 @@ export default function TheStack() { key: "uranium", title: "Uranium & Fuel Cycle", icon: Zap, - color: "#fb923c", + color: CATEGORY_COLORS.uranium, description: "Uranium miners and fuel cycle companies supplying the nuclear renaissance.", tooltip: "Uranium spot ~$92/lb (Mar 2026). Cameco is the largest public miner with direct spot beta. NexGen's Rook I is the highest-grade undeveloped uranium deposit. Centrus is the only US-licensed HALEU producer.", }, @@ -292,7 +294,7 @@ export default function TheStack() { key: "powerHardware", title: "Power Hardware", icon: Server, - color: "#F0A500", + color: CATEGORY_COLORS.power, description: "Transformers, switchgear, cooling, and electrical equipment.", tooltip: "GE Vernova's turbine order book leads DC buildout pace. Eaton is at max switchgear/transformer capacity. Vertiv is the fastest-growing power/cooling infrastructure company. Transformer shortages remain the primary bottleneck on DC energization.", }, @@ -300,7 +302,7 @@ export default function TheStack() { key: "utilities", title: "Utilities", icon: Zap, - color: "#34d399", + color: CATEGORY_COLORS.utilities, description: "Utilities signing long-term power agreements with hyperscalers.", tooltip: "Dominion serves Northern Virginia (70% of global internet traffic). NextEra signed a 2.5 GW deal with Meta. Southern Company's Georgia territory is the center of Southeast DC growth. Regulated utilities benefit from structurally rising electricity demand.", }, @@ -308,7 +310,7 @@ export default function TheStack() { key: "dataCenters", title: "Data Centers", icon: Server, - color: "#a855f7", + color: CATEGORY_COLORS.datacenters, description: "REITs and colocation operators. Direct proxies for AI capacity buildout.", tooltip: "Equinix operates 273 data centers across 77 markets. Digital Realty has 300+ facilities globally. IREN is pivoting from Bitcoin mining to GPU-as-a-Service. Power contracts and land-bank are the critical metrics.", }, @@ -316,7 +318,7 @@ export default function TheStack() { key: "construction", title: "Construction & EPC", icon: Server, - color: "#f472b6", + color: CATEGORY_COLORS.construction, description: "Electrical contractors and engineers building grid connections for AI campuses.", tooltip: "Quanta is the largest electrical utility contractor in North America, building transmission lines and substations for DC campuses. EMCOR has a record $4.3B backlog. Sterling Infrastructure has 125% YoY DC revenue growth.", }, @@ -324,7 +326,7 @@ export default function TheStack() { key: "rawMaterialsMining", title: "Raw Materials - Mining & Metals", icon: Server, - color: "#d97706", + color: INK.secondary, // periphery tier: gray = supporting layer, hues are reserved for the 10 thesis layers description: "Copper, steel, and rare earth producers supplying data center and grid buildout.", tooltip: "Copper is the essential conductor in every transformer, busbar, and cable connecting grid to rack. Steel is the structural backbone of data center campuses. Rare earths power wind turbines and EV motors in the energy transition.", }, @@ -332,7 +334,7 @@ export default function TheStack() { key: "rawMaterialsNatGas", title: "Raw Materials - Natural Gas", icon: Zap, - color: "#0ea5e9", + color: CATEGORY_COLORS.gas, description: "Natural gas producers fueling bridge power generation for data centers.", tooltip: "Gas-fired generation is the bridge fuel while nuclear and renewables scale. Appalachian and Haynesville producers benefit from rising gas demand as hyperscalers seek reliable, dispatchable power generation capacity.", }, @@ -340,7 +342,7 @@ export default function TheStack() { key: "renewableGeneration", title: "Renewable Generation", icon: Zap, - color: "#10b981", + color: CATEGORY_COLORS.renewables, description: "Solar manufacturers and renewable energy companies powering clean data center commitments.", tooltip: "Hyperscalers have committed to 100% renewable energy targets. First Solar is the largest US panel maker. AES has signed multi-GW PPAs with Google and Microsoft. Solar and wind are the fastest-growing power sources for data center operations.", }, @@ -348,7 +350,7 @@ export default function TheStack() { key: "transmissionGrid", title: "Transmission & Grid Hardware", icon: Server, - color: "#8b5cf6", + color: CATEGORY_COLORS.grid, description: "Wire, generators, and grid equipment connecting power to data center campuses.", tooltip: "Every data center requires extensive copper wiring (Encore Wire), backup generators (Generac), and electrical infrastructure. Grid interconnection is the bottleneck for new data center energization timelines.", }, @@ -356,7 +358,7 @@ export default function TheStack() { key: "cryptoAIDC", title: "Crypto/AI DC Operators", icon: Cpu, - color: "#ec4899", + color: INK.secondary, // periphery tier (non-adjacent to mining in display order, always labeled) description: "Bitcoin miners pivoting infrastructure and power contracts toward AI/HPC hosting.", tooltip: "CleanSpark and MARA Holdings are the largest public Bitcoin miners exploring AI/HPC hosting. Their existing power contracts, cooling infrastructure, and facility footprints are directly transferable to GPU-as-a-Service operations.", }, @@ -364,7 +366,7 @@ export default function TheStack() { key: "etfsBenchmarks", title: "ETF Benchmarks", icon: TrendingUp, - color: "#6b7280", + color: INK.muted, // benchmarks are neutral, not a category description: "Sector ETFs for uranium, data centers, grid infrastructure, and utilities.", tooltip: "URA and URNM track uranium mining. DTCR tracks data center/digital infrastructure. GRID tracks smart grid companies. XLU tracks utilities. Compare individual picks against these benchmarks.", }, @@ -378,7 +380,7 @@ export default function TheStack() {

The Stack

100+ equities across 13 layers of the AI power supply chain. Intraday prices via Yahoo Finance.

- + Yahoo Finance · Live
@@ -393,7 +395,7 @@ export default function TheStack() { data-testid={`timeframe-${tf.toLowerCase()}`} className={`px-3 py-1 text-xs font-mono font-semibold rounded transition-all ${ timeframe === tf - ? "bg-[#F07800] text-white" + ? "bg-brand text-white" : "text-muted-foreground hover:text-foreground" }`} > @@ -504,7 +506,7 @@ export default function TheStack() { {data?.correlationCoeff !== undefined && (

CCJ Pearson r

-

{data.correlationCoeff.toFixed(3)}

+

{data.correlationCoeff.toFixed(3)}

{data.correlationCoeff > 0.7 ? "Strong" : data.correlationCoeff > 0.4 ? "Moderate" : "Weak"} correlation

@@ -526,33 +528,30 @@ export default function TheStack() { <> - + } /> {/* Upper confidence band */} null as any} legendType="none" name="Upper Band" @@ -561,7 +560,7 @@ export default function TheStack() { null as any} legendType="none" name="Lower Band" @@ -570,7 +569,7 @@ export default function TheStack() { null as any} legendType="none" name="OLS Fit" @@ -578,7 +577,7 @@ export default function TheStack() { {/* Raw scatter dots */}
-
+
Weekly observation
-
+
OLS trend line
-
+
±1.5σ channel
@@ -604,10 +603,10 @@ export default function TheStack() {

- CCJ (pure miner) has higher uranium spot beta. Its P&L moves directly with U3O8 pricing. + CCJ (pure miner) has higher uranium spot beta. Its P&L moves directly with U3O8 pricing.

- CEG (nuclear utility) is influenced by electricity contracts and regulated returns. Smoother, less volatile nuclear exposure. + CEG (nuclear utility) is influenced by electricity contracts and regulated returns. Smoother, less volatile nuclear exposure.

diff --git a/client/src/pages/compute-frontier.tsx b/client/src/pages/compute-frontier.tsx index bc2ecdb..985e910 100644 --- a/client/src/pages/compute-frontier.tsx +++ b/client/src/pages/compute-frontier.tsx @@ -22,6 +22,8 @@ import { Cell, } from "recharts"; import { Cpu, Zap, ArrowUpDown, ExternalLink, Atom, MapPin } from "lucide-react"; +import { BRAND, INK, SURFACE, CATEGORY_COLORS, STATUS_COLORS } from "@/lib/tokens"; +import { axisProps, gridProps } from "@/lib/chart-theme"; // ─── Types (mirror /api/clusters and /api/clusters/metrics) ──────────────── @@ -81,19 +83,15 @@ interface ClusterMetrics { // ─── Display helpers ─────────────────────────────────────────────────────── -const STATUS_COLOR: Record = { - operational: "#F07800", - construction: "#F0A500", - announced: "rgba(255,255,255,0.45)", -}; +const STATUS_COLOR: Record = STATUS_COLORS; const ENERGY_COLOR: Record = { - nuclear: "#a855f7", - "on-site gas": "#F0A500", - grid: "#7B8FA1", - hydro: "#1E90FF", - renewables: "#22c55e", - other: "#6b7280", + nuclear: CATEGORY_COLORS.nuclear, + "on-site gas": CATEGORY_COLORS.gas, + grid: CATEGORY_COLORS.grid, + hydro: CATEGORY_COLORS.hydro, + renewables: CATEGORY_COLORS.renewables, + other: INK.faint, }; const STATUS_LABEL: Record = { @@ -121,7 +119,7 @@ function gpuCell(n: number | null): string { /** Small "est." tag for any value whose field is in the cluster's estimated[]. */ function Est({ on }: { on: boolean }) { if (!on) return null; - return est.; + return est.; } type SortKey = "name" | "operator" | "plannedPowerMW" | "ratedPowerMW" | "gpuCount" | "onlineDate"; @@ -208,24 +206,24 @@ export default function ComputeFrontier() {
- +

Compute Frontier

The named AI training and inference superclusters being built across the US, by GPUs, chips, and power, tied to the nuclear-for-AI deals GridTilt already tracks. This registry is tracked, not exhaustive. Power is in MW. Every figure that is a GridTilt estimate or an announced target carries an{" "} - est. tag; GPU counts are shown only where an operator has + est. tag; GPU counts are shown only where an operator has disclosed them.

-
+
{metrics?.lastRefreshed &&
refreshed {metrics.lastRefreshed}
} - + Power Map
- + Nuclear deals
@@ -250,95 +248,95 @@ export default function ComputeFrontier() { {metrics ? ( - - `${(v / 1000).toFixed(0)}`} /> - - [`${v.toLocaleString()} MW`, `${p.payload.count} clusters`]} cursor={{ fill: "rgba(240,120,0,0.06)" }} /> - + + `${(v / 1000).toFixed(0)}`} /> + + [`${v.toLocaleString()} MW`, `${p.payload.count} clusters`]} cursor={{ fill: BRAND.glow }} /> + ) : } -

x axis in GW{metrics && metrics.concentration.operatorCount > OP_TOP_N ? ` · ${metrics.concentration.operatorCount} operators total` : ""}

+

x axis in GW{metrics && metrics.concentration.operatorCount > OP_TOP_N ? ` · ${metrics.concentration.operatorCount} operators total` : ""}

{metrics ? ( - - - `${(v / 1000).toFixed(0)}`} /> - [`${v.toLocaleString()} MW`, "planned"]} cursor={{ fill: "rgba(240,120,0,0.06)" }} /> - + + + `${(v / 1000).toFixed(0)}`} /> + [`${v.toLocaleString()} MW`, "planned"]} cursor={{ fill: BRAND.glow }} /> + ) : } -

y axis in GW

+

y axis in GW

{metrics ? ( - - - `${(v / 1000).toFixed(0)}`} /> - [`${v.toLocaleString()} MW`, `${p.payload.count} clusters`]} cursor={{ fill: "rgba(240,120,0,0.06)" }} /> + + + `${(v / 1000).toFixed(0)}`} /> + [`${v.toLocaleString()} MW`, `${p.payload.count} clusters`]} cursor={{ fill: BRAND.glow }} /> - {metrics.byStatus.map((s) => )} + {metrics.byStatus.map((s) => )} ) : } -

y axis in GW

+

y axis in GW

{clusters ? ( - - - - [`${v} GW`, "planned online"]} cursor={{ fill: "rgba(240,120,0,0.06)" }} /> - + + + + [`${v} GW`, "planned online"]} cursor={{ fill: BRAND.glow }} /> + ) : } -

bucketed by first announced year

+

bucketed by first announced year

{metrics ? ( - - `${(v / 1000).toFixed(0)}`} /> - - [`${v.toLocaleString()} MW`, `${p.payload.count} clusters`]} cursor={{ fill: "rgba(240,120,0,0.06)" }} /> + + `${(v / 1000).toFixed(0)}`} /> + + [`${v.toLocaleString()} MW`, `${p.payload.count} clusters`]} cursor={{ fill: BRAND.glow }} /> - {metrics.byEnergySource.map((e) => )} + {metrics.byEnergySource.map((e) => )} ) : } -

x axis in GW · grid vs behind-the-meter gas, nuclear, renewables

+

x axis in GW · grid vs behind-the-meter gas, nuclear, renewables

{/* Map */} -
- Cluster map -
+
+ Cluster map +
operational construction - announced + announced
{clusters && clusters.length > 0 ? ( - + ({ value: o, label: o }))]} /> ({ value: i, label: i }))]} /> -