siddhant-rajhans commited on
Commit
25b3b03
·
1 Parent(s): 51415ee

TRIBE-style landing redesign with black/white theme toggle

Browse files

Rebuild the home page to match the Meta TRIBE v2 aidemos layout: branded
top header with theme toggle, two-column hero (architecture steps +
pipeline diagram on the left, interactive demo card on the right),
stats bar, and a polished four-column footer.

* New tribe.py module with top_header, architecture_steps,
pipeline_diagram, segmented, demo_card_open/close, and tr_footer.
* Theme system reworked into base theme.css plus theme_black.css and
theme_white.css overlays driven by session state. Toggle navigates
via ?theme=<mode> query param so the swap is one click.
* feature_card_link uses span children inside the anchor so the bleach
sanitizer no longer hoists block elements out of the link, fixing
the feature card grid.
* Segmented control inputs now use the sr-only pattern instead of
display:none so BaseWeb radio onChange fires on click. Eye Open/Close,
True/Compare/Predicted, and Normal/Inflated all visibly drive the
3D brain again.
* hot-on-cortex colormap so the cortex stays visible on either theme
(neutral gray base with hot ramp above 0.20).
* Streamlit config flipped to pure black background to remove the
purple haze that fought the white theme.
* Dockerfile entrypoint switched from app.py to Home.py so the
HuggingFace Space serves the redesigned landing.

Files changed (9) hide show
  1. .streamlit/config.toml +2 -2
  2. Dockerfile +1 -1
  3. Home.py +276 -139
  4. assets/theme.css +716 -2
  5. assets/theme_black.css +60 -0
  6. assets/theme_white.css +245 -0
  7. brain_mesh.py +80 -10
  8. theme.py +175 -54
  9. tribe.py +362 -0
.streamlit/config.toml CHANGED
@@ -1,8 +1,8 @@
1
  [theme]
2
  base = "dark"
3
  primaryColor = "#7C3AED"
4
- backgroundColor = "#050510"
5
- secondaryBackgroundColor = "#0F0F23"
6
  textColor = "#F1F5F9"
7
  font = "sans serif"
8
 
 
1
  [theme]
2
  base = "dark"
3
  primaryColor = "#7C3AED"
4
+ backgroundColor = "#000000"
5
+ secondaryBackgroundColor = "#0A0A0A"
6
  textColor = "#F1F5F9"
7
  font = "sans serif"
8
 
Dockerfile CHANGED
@@ -13,4 +13,4 @@ COPY . .
13
 
14
  EXPOSE 7860
15
 
16
- CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.headless=true", "--server.address=0.0.0.0"]
 
13
 
14
  EXPOSE 7860
15
 
16
+ CMD ["streamlit", "run", "Home.py", "--server.port=7860", "--server.headless=true", "--server.address=0.0.0.0"]
Home.py CHANGED
@@ -1,159 +1,296 @@
1
- """CortexLab Dashboard - Futuristic Landing Page."""
 
 
2
 
3
- import streamlit as st
4
  import numpy as np
 
5
 
6
- from theme import inject_theme, hero_header, glow_card, section_header, feature_card
7
- from session import init_session, show_analysis_log
 
 
 
 
 
 
8
  from utils import make_roi_indices
9
- from brain_mesh import load_fsaverage_mesh, generate_sample_activations, render_interactive_3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- st.set_page_config(page_title="CortexLab", page_icon="🧠", layout="wide", initial_sidebar_state="collapsed")
 
 
 
 
 
12
  init_session()
13
  inject_theme()
14
 
15
- # --- Hero ---
16
- hero_header(
17
- "CortexLab",
18
- "Enhanced multimodal fMRI brain encoding toolkit built on Meta's TRIBE v2",
 
19
  )
20
 
21
- # --- 3D Brain Hero ---
22
- st.markdown("<div style='height: 0.5rem'></div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- with st.spinner("Rendering brain..."):
25
- coords, faces = load_fsaverage_mesh("left", "fsaverage5")
26
- n_verts = coords.shape[0]
27
- roi_indices, _ = make_roi_indices()
28
- mesh_roi = {name: np.clip((idx * n_verts // 580).astype(int), 0, n_verts - 1) for name, idx in roi_indices.items()}
29
- activations = generate_sample_activations(n_verts, mesh_roi, "multimodal", seed=42)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- fig = render_interactive_3d(
32
- coords, faces, activations, cmap="Inferno", vmin=0, vmax=0.8,
33
- bg_color="#050510", initial_view="Lateral Left",
34
- roi_indices=mesh_roi, show_labels=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  )
36
- if fig is not None:
37
- fig.update_layout(
38
- height=500,
39
- margin=dict(l=0, r=0, t=0, b=0),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  )
41
- st.plotly_chart(fig, use_container_width=True)
42
 
43
- # --- Stats Bar ---
44
- st.markdown("<div style='height: 0.5rem'></div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  c1, c2, c3, c4, c5 = st.columns(5)
46
- with c1: glow_card("Tests", "89", "All passing", "#10B981")
47
- with c2: glow_card("Analysis Modules", "10", "Brain encoding", "#7C3AED")
48
  with c3: glow_card("ROIs", "29", "HCP MMP1.0", "#3B82F6")
49
- with c4: glow_card("Contributors", "4", "Open source", "#EC4899")
50
- with c5: glow_card("License", "CC BY-NC", "Non-commercial", "#F59E0B")
51
 
52
- # --- Feature Grid ---
53
  st.markdown("<div style='height: 1.5rem'></div>", unsafe_allow_html=True)
54
- section_header("Analysis Tools", "Everything you need for computational neuroscience research")
55
-
56
- col1, col2, col3 = st.columns(3)
57
- with col1:
58
- st.markdown(feature_card(
59
- "🎯", "Brain Alignment Benchmark",
60
- "Score any AI model against brain responses. RSA, CKA, Procrustes with permutation tests, bootstrap CIs, and FDR correction.",
61
- "#7C3AED"
62
- ), unsafe_allow_html=True)
63
- st.page_link("pages/1_Brain_Alignment.py", label="Open Brain Alignment")
64
-
65
- with col2:
66
- st.markdown(feature_card(
67
- "📊", "Cognitive Load Scorer",
68
- "Predict cognitive demand across visual, auditory, language, and executive dimensions with confidence bands.",
69
- "#3B82F6"
70
- ), unsafe_allow_html=True)
71
- st.page_link("pages/2_Cognitive_Load.py", label="Open Cognitive Load")
72
-
73
- with col3:
74
- st.markdown(feature_card(
75
- "⏱️", "Temporal Dynamics",
76
- "Peak response latency, lag correlations, sustained vs transient decomposition, cross-ROI lag matrix.",
77
- "#06B6D4"
78
- ), unsafe_allow_html=True)
79
- st.page_link("pages/3_Temporal_Dynamics.py", label="Open Temporal Dynamics")
80
-
81
- col4, col5, col6 = st.columns(3)
82
- with col4:
83
- st.markdown(feature_card(
84
- "🔗", "ROI Connectivity",
85
- "Functional connectivity, partial correlation, network clustering, modularity, degree and betweenness centrality.",
86
- "#10B981"
87
- ), unsafe_allow_html=True)
88
- st.page_link("pages/4_Connectivity.py", label="Open Connectivity")
89
-
90
- with col5:
91
- st.markdown(feature_card(
92
- "🧠", "3D Brain Viewer",
93
- "Interactive rotatable brain surface with activation overlays, publication-quality multi-view panels, ROI highlighting.",
94
- "#EC4899"
95
- ), unsafe_allow_html=True)
96
- st.page_link("pages/5_Brain_Viewer.py", label="Open Brain Viewer")
97
-
98
- with col6:
99
- st.markdown(feature_card(
100
- "🔴", "Live Inference",
101
- "Real-time brain prediction from webcam, screen capture, or video. All metrics update live. Works in simulation mode or with full CortexLab + GPU.",
102
- "#EF4444"
103
- ), unsafe_allow_html=True)
104
- st.page_link("pages/6_Live_Inference.py", label="Open Live Inference")
105
-
106
- # --- Data Config (collapsed) ---
107
- st.markdown("<div style='height: 1rem'></div>", unsafe_allow_html=True)
108
- with st.expander("Data Configuration", expanded=False):
109
- from session import data_summary_widget, upload_npy_widget
110
- col_src, col_params = st.columns([1, 2])
111
-
112
- with col_src:
113
- source = st.radio("Data source", ["Synthetic (realistic)", "Upload your data"], index=0)
114
- st.session_state["data_source"] = "synthetic" if "Synthetic" in source else "uploaded"
115
-
116
- with col_params:
117
- if st.session_state["data_source"] == "synthetic":
118
- c1, c2, c3, c4 = st.columns(4)
119
- st.session_state["stimulus_type"] = c1.selectbox("Stimulus", ["visual", "auditory", "language", "multimodal"])
120
- st.session_state["n_timepoints"] = c2.slider("TRs", 30, 200, 80)
121
- st.session_state["tr_seconds"] = c3.slider("TR (s)", 0.5, 2.0, 1.0, 0.1)
122
- st.session_state["seed"] = c4.number_input("Seed", value=42, min_value=0)
123
-
124
- roi_indices_full, n_vertices = make_roi_indices()
125
- st.session_state["roi_indices"] = roi_indices_full
126
- st.session_state["n_vertices"] = n_vertices
127
-
128
- from synthetic import generate_realistic_predictions
129
- predictions = generate_realistic_predictions(
130
- st.session_state["n_timepoints"], roi_indices_full,
131
- st.session_state["stimulus_type"], st.session_state["tr_seconds"],
132
- seed=st.session_state["seed"],
133
  )
134
- st.session_state["brain_predictions"] = predictions
135
- else:
136
- uploaded = upload_npy_widget("Upload predictions (.npy)", "upload_home")
137
- if uploaded is not None:
138
- st.session_state["brain_predictions"] = uploaded
139
- roi_indices_full, _ = make_roi_indices()
140
- st.session_state["roi_indices"] = roi_indices_full
141
-
142
- preds = st.session_state.get("brain_predictions")
143
- roi_idx = st.session_state.get("roi_indices")
144
- if preds is not None and roi_idx is not None:
145
- data_summary_widget(preds, roi_idx)
146
-
147
- # --- Footer ---
148
- show_analysis_log()
149
- st.markdown("""
150
- <div style="text-align: center; padding: 2rem 0 1rem 0; color: #475569; font-size: 0.75rem;">
151
- Built on <a href="https://github.com/facebookresearch/tribev2" style="color: #7C3AED; text-decoration: none;">Meta's TRIBE v2</a>
152
- &nbsp;&bull;&nbsp;
153
- <a href="https://github.com/siddhant-rajhans/cortexlab" style="color: #3B82F6; text-decoration: none;">GitHub</a>
154
- &nbsp;&bull;&nbsp;
155
- <a href="https://huggingface.co/SID2000/cortexlab" style="color: #06B6D4; text-decoration: none;">HuggingFace</a>
156
- &nbsp;&bull;&nbsp;
157
- CC BY-NC 4.0
158
- </div>
159
- """, unsafe_allow_html=True)
 
1
+ """CortexLab Dashboard - TRIBE-style landing page."""
2
+
3
+ from __future__ import annotations
4
 
 
5
  import numpy as np
6
+ import streamlit as st
7
 
8
+ from theme import (
9
+ inject_theme,
10
+ glow_card,
11
+ section_header,
12
+ feature_card_link,
13
+ get_theme_mode,
14
+ )
15
+ from session import init_session
16
  from utils import make_roi_indices
17
+ from brain_mesh import (
18
+ load_fsaverage_mesh,
19
+ generate_sample_activations,
20
+ render_interactive_3d,
21
+ make_hot_on_cortex_colorscale,
22
+ )
23
+ from tribe import (
24
+ top_header,
25
+ architecture_steps,
26
+ pipeline_diagram,
27
+ segmented,
28
+ demo_card_open,
29
+ demo_card_close,
30
+ tr_footer,
31
+ )
32
+
33
 
34
+ st.set_page_config(
35
+ page_title="CortexLab",
36
+ page_icon="🧠",
37
+ layout="wide",
38
+ initial_sidebar_state="collapsed",
39
+ )
40
  init_session()
41
  inject_theme()
42
 
43
+ # --- Top header bar -------------------------------------------------------
44
+ top_header(
45
+ title="CortexLab",
46
+ subtitle="Multimodal fMRI brain encoding",
47
+ parent="Stevens · built on Meta TRIBE v2",
48
  )
49
 
50
+ # --- Two-column hero ------------------------------------------------------
51
+ left, right = st.columns([1.0, 1.05], gap="large")
52
+
53
+ with left:
54
+ architecture_steps(
55
+ title="CortexLab: a multimodal encoding pipeline",
56
+ intro="CortexLab predicts brain activity through a three-stage pipeline:",
57
+ steps=[
58
+ (
59
+ "Multimodal feature extraction",
60
+ "Pretrained <u>CLIP ViT-L/14</u>, <u>V-JEPA 2</u>, and <u>CLIP-text</u> "
61
+ "encoders extract vision, motion, and language embeddings from short video "
62
+ "clips and machine-generated captions.",
63
+ ),
64
+ (
65
+ "Voxelwise ridge encoding",
66
+ "A fused <u>Triton</u> kernel solves <u>327,684</u> independent voxelwise "
67
+ "ridge regressions in seconds, mapping each modality's features to per-voxel "
68
+ "BOLD responses on the fsaverage cortical surface.",
69
+ ),
70
+ (
71
+ "Causal modality lesion",
72
+ "Each modality is ablated at test time and per-voxel ΔR² is permutation-tested "
73
+ "across <u>1,000 shuffles</u>, BH-FDR corrected, and rendered onto the "
74
+ "inflated cortex.",
75
+ ),
76
+ ],
77
+ )
78
+ pipeline_diagram()
79
 
80
+ with right:
81
+ demo_card_open(title="Live encoder · sample stimulus")
82
+
83
+ # Segmented control row 1 (True/Compare/Predicted) and toggles.
84
+ st.markdown('<div class="tr-segmented">', unsafe_allow_html=True)
85
+ c1, c2, c3 = st.columns([1.2, 0.9, 1.0])
86
+ with c1:
87
+ view_mode = segmented(
88
+ "view mode", ["True", "Compare", "Predicted"],
89
+ default_index=2, key="tr_view_mode",
90
+ )
91
+ with c2:
92
+ eye_mode = segmented(
93
+ "eye", ["Open", "Close"], default_index=0, key="tr_eye_mode",
94
+ )
95
+ with c3:
96
+ infl_mode = segmented(
97
+ "inflate", ["Normal", "Inflated"], default_index=1, key="tr_infl_mode",
98
+ )
99
+ st.markdown("</div>", unsafe_allow_html=True)
100
+
101
+ # 3D brain render. Uses a custom hot-on-cortex colormap so the cortex
102
+ # shape stays visible on either theme. Each control wires to something
103
+ # the user can actually see change:
104
+ #
105
+ # view_mode -> ACTIVATION PATTERN
106
+ # True = "ground-truth" visual pattern (V1/V2/V4 dominant)
107
+ # Predicted = model's multimodal prediction (LO/MT/FFC active)
108
+ # Compare = residual (predicted minus true), magnitude
109
+ #
110
+ # eye_mode -> visual-cortex MAGNITUDE
111
+ # Open = full activation (eyes seeing the stimulus)
112
+ # Close = visual ROIs muted to baseline (eyes shut)
113
+ #
114
+ # infl_mode -> CORTICAL SURFACE
115
+ # Normal = pial mesh (real anatomical shape)
116
+ # Inflated = inflated mesh (encoding-paper convention)
117
+ theme_mode = get_theme_mode()
118
+ brain_bg = "#000000" if theme_mode == "black" else "#FFFFFF"
119
+
120
+ with st.spinner("Rendering brain..."):
121
+ surface = "inflated" if infl_mode == "Inflated" else "pial"
122
+ coords, faces = load_fsaverage_mesh(
123
+ "left", "fsaverage5", surface=surface,
124
+ )
125
+ n_verts = coords.shape[0]
126
+ roi_indices, _ = make_roi_indices()
127
+ mesh_roi = {
128
+ name: np.clip((idx * n_verts // 580).astype(int), 0, n_verts - 1)
129
+ for name, idx in roi_indices.items()
130
+ }
131
+
132
+ # Pattern selector for the view mode.
133
+ pattern_for_mode = {
134
+ "True": "visual", # what the brain "really" does for video
135
+ "Predicted": "multimodal", # what our encoder predicts
136
+ "Compare": "language", # contrasting pattern -> reads as residual
137
+ }.get(view_mode, "multimodal")
138
+ seed = {"True": 7, "Predicted": 42, "Compare": 11}.get(view_mode, 42)
139
+ activations = generate_sample_activations(
140
+ n_verts, mesh_roi, pattern_for_mode, seed=seed,
141
+ )
142
 
143
+ # Eye-state modulation: closing the eyes mutes visual cortex.
144
+ if eye_mode == "Close":
145
+ for roi_name in ("V1", "V2", "V3", "V4", "MT", "MST", "FFC", "VVC"):
146
+ idx = mesh_roi.get(roi_name)
147
+ if idx is not None and len(idx) > 0:
148
+ activations[idx] *= 0.15
149
+ activations = np.clip(activations, 0, 1)
150
+
151
+ cortex_cmap = make_hot_on_cortex_colorscale(theme_mode)
152
+ fig = render_interactive_3d(
153
+ coords, faces, activations,
154
+ cmap=cortex_cmap, vmin=0, vmax=0.8,
155
+ bg_color=brain_bg,
156
+ initial_view="Lateral Left",
157
+ roi_indices=mesh_roi,
158
+ show_labels=False,
159
+ )
160
+ if fig is not None:
161
+ fig.update_layout(height=420, margin=dict(l=0, r=0, t=0, b=0))
162
+ st.plotly_chart(fig, use_container_width=True)
163
+
164
+ # Tabs row (Examples / Performance / In-Silico / Multimodality).
165
+ tab_examples, tab_perf, tab_silico, tab_multi = st.tabs(
166
+ ["Examples", "Performance", "In-Silico", "Multimodality"]
167
  )
168
+
169
+ with tab_examples:
170
+ st.markdown(
171
+ """
172
+ <div style="padding: 0.6rem 0.2rem; color: var(--text-secondary); font-size: 0.85rem;">
173
+ Sample stimuli from BOLD Moments. Click a clip to see the predicted brain response.
174
+ </div>
175
+ """,
176
+ unsafe_allow_html=True,
177
+ )
178
+ _tile_style = (
179
+ "aspect-ratio: 16/9; background: rgba(255,255,255,0.04); "
180
+ "border: 1px dashed rgba(255,255,255,0.1); border-radius: 8px; "
181
+ "display: flex; align-items: center; justify-content: center; "
182
+ "color: var(--text-secondary); font-size: 0.8rem;"
183
+ )
184
+ ex_a, ex_b, ex_c = st.columns(3)
185
+ with ex_a:
186
+ st.markdown(f"<div style='{_tile_style}'>aerobics.mp4</div>",
187
+ unsafe_allow_html=True)
188
+ with ex_b:
189
+ st.markdown(f"<div style='{_tile_style}'>swimming.mp4</div>",
190
+ unsafe_allow_html=True)
191
+ with ex_c:
192
+ st.markdown(f"<div style='{_tile_style}'>cooking.mp4</div>",
193
+ unsafe_allow_html=True)
194
+
195
+ with tab_perf:
196
+ st.markdown(
197
+ """
198
+ <div style="padding: 0.4rem 0.2rem; color: var(--text-secondary); font-size: 0.85rem;">
199
+ Group-mean ROI breakdown of FDR-significant ΔR² (n = 10 subjects, 1,000 perms).
200
+ </div>
201
+ """,
202
+ unsafe_allow_html=True,
203
+ )
204
+ st.markdown(
205
+ """
206
+ | ROI | q-sig (vision) % | q-sig (text) % |
207
+ |---|---:|---:|
208
+ | MT (motion) | **94%** | 40% |
209
+ | MST | **93%** | 35% |
210
+ | LO 1-3 (lateral occipital) | **87-91%** | 13-25% |
211
+ | FFC (face complex) | **77%** | 25% |
212
+ | PH (place) | **74%** | 23% |
213
+ | V4 | **68%** | 7% |
214
+ | V3 | 46% | 3% |
215
+ | V2 | 31% | 3% |
216
+ | V1 | 25% | 2% |
217
+ | area 44 (Broca) | 3% | 1% |
218
+ | A1 (auditory) | 4% | 1% |
219
+ """
220
+ )
221
+
222
+ with tab_silico:
223
+ st.markdown(
224
+ """
225
+ <div style="padding: 0.4rem 0.2rem; color: var(--text-secondary); font-size: 0.85rem;">
226
+ Drop in any video / audio / text — CortexLab returns the predicted cortical
227
+ response without scanning anyone.
228
+ </div>
229
+ """,
230
+ unsafe_allow_html=True,
231
+ )
232
+ st.file_uploader(
233
+ "Stimulus", type=["mp4", "mov", "wav", "mp3", "txt"],
234
+ label_visibility="collapsed", key="tr_silico_upload",
235
  )
 
236
 
237
+ with tab_multi:
238
+ st.markdown(
239
+ """
240
+ <div style="padding: 0.4rem 0.2rem; color: var(--text-secondary); font-size: 0.85rem;">
241
+ Vision + text now. Audio (Wav2Vec / HuBERT) and motion (V-JEPA 2) are next.
242
+ Each modality is causally testable via the lesion protocol.
243
+ </div>
244
+ """,
245
+ unsafe_allow_html=True,
246
+ )
247
+
248
+ demo_card_close()
249
+
250
+ # --- Stats bar ------------------------------------------------------------
251
+ st.markdown("<div style='height: 1.5rem'></div>", unsafe_allow_html=True)
252
  c1, c2, c3, c4, c5 = st.columns(5)
253
+ with c1: glow_card("Tests", "280", "All passing", "#10B981")
254
+ with c2: glow_card("Subjects", "10", "BOLD Moments", "#7C3AED")
255
  with c3: glow_card("ROIs", "29", "HCP MMP1.0", "#3B82F6")
256
+ with c4: glow_card("Permutations", "1,000", "BH-FDR", "#EC4899")
257
+ with c5: glow_card("Vision q-sig", "94%", "in MT", "#F59E0B")
258
 
259
+ # --- Pages grid -----------------------------------------------------------
260
  st.markdown("<div style='height: 1.5rem'></div>", unsafe_allow_html=True)
261
+ section_header("Analysis tools", "Each page is a focused workflow on the same encoder")
262
+
263
+ _TOOLS = [
264
+ ("target", "Brain Alignment Benchmark",
265
+ "Score any AI model against brain responses. RSA, CKA, Procrustes with permutation tests, bootstrap CIs, FDR correction.",
266
+ "#7C3AED", "./Brain_Alignment"),
267
+ ("bars", "Cognitive Load Scorer",
268
+ "Predict cognitive demand across visual, auditory, language, and executive dimensions with confidence bands.",
269
+ "#3B82F6", "./Cognitive_Load"),
270
+ ("clock", "Temporal Dynamics",
271
+ "Peak response latency, lag correlations, sustained vs transient decomposition, cross-ROI lag matrix.",
272
+ "#06B6D4", "./Temporal_Dynamics"),
273
+ ("graph", "ROI Connectivity",
274
+ "Functional connectivity, partial correlation, network clustering, modularity, centrality.",
275
+ "#10B981", "./Connectivity"),
276
+ ("brain", "3D Brain Viewer",
277
+ "Interactive rotatable brain surface with activation overlays, publication-quality multi-view panels, ROI highlighting.",
278
+ "#EC4899", "./Brain_Viewer"),
279
+ ("broadcast", "Live Inference",
280
+ "Real-time brain prediction from webcam, screen capture, or video. Updates the 3D brain live.",
281
+ "#EF4444", "./Live_Inference"),
282
+ ]
283
+ for row in (_TOOLS[:3], _TOOLS[3:]):
284
+ cols = st.columns(3, gap="medium")
285
+ for col, (icon, title, desc, color, href) in zip(cols, row):
286
+ with col:
287
+ st.markdown(
288
+ feature_card_link(icon, title, desc, href, color),
289
+ unsafe_allow_html=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  )
291
+
292
+ # --- Footer ---------------------------------------------------------------
293
+ tr_footer(
294
+ title="CortexLab",
295
+ tagline="Multimodal fMRI brain encoding · open-source toolkit built on Meta's TRIBE v2.",
296
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/theme.css CHANGED
@@ -20,8 +20,8 @@
20
  --glow-cyan: 0 0 15px rgba(6, 182, 212, 0.2);
21
  }
22
 
23
- /* === HIDE STREAMLIT BRANDING === */
24
- #MainMenu, footer, header {
25
  visibility: hidden;
26
  }
27
 
@@ -33,6 +33,21 @@ div[data-testid="stDecoration"] {
33
  display: none;
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  /* === BASE === */
37
  .stApp {
38
  background: var(--bg-primary);
@@ -297,3 +312,702 @@ code {
297
  border-radius: 4px;
298
  color: var(--accent-cyan);
299
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  --glow-cyan: 0 0 15px rgba(6, 182, 212, 0.2);
21
  }
22
 
23
+ /* === HIDE STREAMLIT BRANDING (but keep the sidebar toggle reachable) === */
24
+ #MainMenu, footer {
25
  visibility: hidden;
26
  }
27
 
 
33
  display: none;
34
  }
35
 
36
+ /* Streamlit's top header contains the sidebar toggle hamburger; we
37
+ * keep it visible but flatten the background so it doesn't conflict
38
+ * with our custom tr-header. */
39
+ header[data-testid="stHeader"] {
40
+ background: transparent !important;
41
+ height: auto;
42
+ }
43
+ header[data-testid="stHeader"]::before { display: none; }
44
+ button[data-testid="stSidebarCollapsedControl"],
45
+ button[data-testid="stSidebarToggle"],
46
+ button[data-testid="stBaseButton-headerNoPadding"] {
47
+ color: var(--text-primary) !important;
48
+ opacity: 0.9;
49
+ }
50
+
51
  /* === BASE === */
52
  .stApp {
53
  background: var(--bg-primary);
 
312
  border-radius: 4px;
313
  color: var(--accent-cyan);
314
  }
315
+
316
+ /* === TRIBE-STYLE HEADER + HERO ====================================== */
317
+
318
+ .tr-header {
319
+ display: grid;
320
+ grid-template-columns: 1fr auto 1fr;
321
+ align-items: center;
322
+ padding: 1.2rem 1.5rem;
323
+ margin: -1rem -1rem 1.5rem -1rem;
324
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
325
+ background: linear-gradient(180deg,
326
+ rgba(5, 5, 16, 0.92) 0%,
327
+ rgba(5, 5, 16, 0.65) 100%);
328
+ backdrop-filter: blur(14px);
329
+ }
330
+
331
+ .tr-header-left { display: flex; flex-direction: column; line-height: 1; }
332
+ .tr-header-title {
333
+ font-size: 1.05rem;
334
+ font-weight: 700;
335
+ color: var(--text-primary);
336
+ letter-spacing: -0.01em;
337
+ }
338
+ .tr-header-subtitle {
339
+ font-size: 0.72rem;
340
+ color: var(--text-secondary);
341
+ margin-top: 0.2rem;
342
+ font-weight: 400;
343
+ }
344
+ .tr-header-center {
345
+ text-align: center;
346
+ color: var(--text-secondary);
347
+ font-size: 0.85rem;
348
+ font-weight: 500;
349
+ letter-spacing: 0.02em;
350
+ opacity: 0.85;
351
+ }
352
+ .tr-header-right {
353
+ text-align: right;
354
+ display: flex;
355
+ justify-content: flex-end;
356
+ gap: 1.5rem;
357
+ }
358
+ .tr-nav-link {
359
+ color: var(--text-primary);
360
+ font-size: 0.85rem;
361
+ font-weight: 500;
362
+ text-decoration: none;
363
+ opacity: 0.75;
364
+ transition: opacity 0.2s ease;
365
+ }
366
+ .tr-nav-link:hover { opacity: 1; }
367
+ .tr-arrow {
368
+ display: inline-block;
369
+ transform: rotate(0deg);
370
+ margin-left: 0.15rem;
371
+ font-size: 0.75rem;
372
+ opacity: 0.6;
373
+ }
374
+
375
+ /* === Architecture explainer card ==================================== */
376
+
377
+ .tr-arch {
378
+ padding: 0.5rem 0.5rem 1rem 0;
379
+ }
380
+ .tr-arch-title {
381
+ font-size: 1.9rem;
382
+ font-weight: 700;
383
+ color: var(--text-primary);
384
+ letter-spacing: -0.02em;
385
+ margin: 0.5rem 0 1rem 0;
386
+ line-height: 1.2;
387
+ }
388
+ .tr-arch-intro {
389
+ color: var(--text-secondary);
390
+ font-size: 0.95rem;
391
+ margin: 0 0 1.5rem 0;
392
+ line-height: 1.55;
393
+ }
394
+ .tr-step-list {
395
+ list-style: none;
396
+ padding: 0;
397
+ margin: 0;
398
+ }
399
+ .tr-step {
400
+ display: flex;
401
+ gap: 1rem;
402
+ margin-bottom: 1.3rem;
403
+ align-items: flex-start;
404
+ }
405
+ .tr-step-num {
406
+ flex-shrink: 0;
407
+ width: 1.6rem;
408
+ height: 1.6rem;
409
+ border-radius: 50%;
410
+ background: rgba(255, 255, 255, 0.08);
411
+ border: 1px solid rgba(255, 255, 255, 0.18);
412
+ display: flex;
413
+ align-items: center;
414
+ justify-content: center;
415
+ font-size: 0.78rem;
416
+ color: var(--text-secondary);
417
+ margin-top: 0.1rem;
418
+ font-feature-settings: "tnum";
419
+ }
420
+ .tr-step-body { flex: 1; }
421
+ .tr-step-title {
422
+ color: var(--text-primary);
423
+ font-size: 0.95rem;
424
+ font-weight: 600;
425
+ display: block;
426
+ margin-bottom: 0.25rem;
427
+ }
428
+ .tr-step-text {
429
+ color: var(--text-secondary);
430
+ font-size: 0.85rem;
431
+ line-height: 1.55;
432
+ margin: 0;
433
+ }
434
+ .tr-step-text u {
435
+ text-decoration: none;
436
+ border-bottom: 1px dotted rgba(124, 58, 237, 0.6);
437
+ color: #C4B5FD;
438
+ }
439
+
440
+ /* === Pipeline diagram =============================================== */
441
+
442
+ .tr-pipeline {
443
+ display: flex;
444
+ align-items: center;
445
+ gap: 0.5rem;
446
+ margin: 1.5rem 0 0 0;
447
+ padding: 1rem;
448
+ background: rgba(15, 15, 35, 0.4);
449
+ border: 1px solid rgba(255, 255, 255, 0.06);
450
+ border-radius: 14px;
451
+ flex-wrap: wrap;
452
+ }
453
+ .tr-pipe-rows {
454
+ display: flex;
455
+ flex-direction: column;
456
+ gap: 0.5rem;
457
+ flex: 1;
458
+ min-width: 240px;
459
+ }
460
+ .tr-pipe-row {
461
+ display: flex;
462
+ align-items: center;
463
+ gap: 0.6rem;
464
+ }
465
+ .tr-pipe-source {
466
+ flex: 1;
467
+ background: rgba(255, 255, 255, 0.04);
468
+ border: 1px solid rgba(255, 255, 255, 0.10);
469
+ border-radius: 8px;
470
+ padding: 0.4rem 0.7rem;
471
+ color: var(--text-primary);
472
+ font-size: 0.8rem;
473
+ display: flex;
474
+ align-items: center;
475
+ gap: 0.5rem;
476
+ }
477
+ .tr-pipe-icon {
478
+ color: var(--text-secondary);
479
+ font-size: 0.8rem;
480
+ width: 1rem;
481
+ text-align: center;
482
+ }
483
+ .tr-pipe-icon-lg {
484
+ font-size: 1.4rem;
485
+ color: var(--text-primary);
486
+ line-height: 1;
487
+ margin-bottom: 0.3rem;
488
+ }
489
+ .tr-pipe-arrow {
490
+ color: var(--text-secondary);
491
+ font-size: 0.85rem;
492
+ opacity: 0.5;
493
+ }
494
+ .tr-pipe-arrow-h { font-size: 1rem; }
495
+ .tr-pipe-model {
496
+ background: rgba(124, 58, 237, 0.10);
497
+ border: 1px solid rgba(124, 58, 237, 0.25);
498
+ color: #DDD6FE;
499
+ border-radius: 8px;
500
+ padding: 0.4rem 0.7rem;
501
+ font-size: 0.78rem;
502
+ font-weight: 500;
503
+ min-width: 100px;
504
+ text-align: center;
505
+ }
506
+ .tr-pipe-fuse {
507
+ background: rgba(59, 130, 246, 0.10);
508
+ border: 1px solid rgba(59, 130, 246, 0.25);
509
+ border-radius: 10px;
510
+ padding: 0.7rem 0.6rem;
511
+ color: #BFDBFE;
512
+ text-align: center;
513
+ min-width: 80px;
514
+ }
515
+ .tr-pipe-label {
516
+ font-size: 0.72rem;
517
+ line-height: 1.2;
518
+ color: var(--text-secondary);
519
+ }
520
+
521
+ /* === Demo panel (right column wrapper) ============================== */
522
+
523
+ .tr-demo-card {
524
+ background: rgba(15, 15, 35, 0.55);
525
+ backdrop-filter: blur(12px);
526
+ border: 1px solid rgba(255, 255, 255, 0.08);
527
+ border-radius: 18px;
528
+ padding: 1rem 1rem 0.5rem 1rem;
529
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4),
530
+ 0 0 0 1px rgba(124, 58, 237, 0.05);
531
+ }
532
+ .tr-demo-header {
533
+ display: flex;
534
+ justify-content: space-between;
535
+ align-items: flex-start;
536
+ margin-bottom: 0.6rem;
537
+ padding: 0.2rem 0.4rem 0.6rem 0.4rem;
538
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
539
+ }
540
+ .tr-demo-title {
541
+ color: var(--text-secondary);
542
+ font-size: 0.78rem;
543
+ text-transform: uppercase;
544
+ letter-spacing: 0.08em;
545
+ font-weight: 600;
546
+ }
547
+ .tr-demo-cbar {
548
+ display: flex;
549
+ align-items: center;
550
+ gap: 0.4rem;
551
+ position: relative;
552
+ }
553
+ .tr-demo-cbar-label {
554
+ font-size: 0.7rem;
555
+ color: var(--text-secondary);
556
+ font-weight: 500;
557
+ }
558
+ .tr-demo-cbar-grad {
559
+ width: 80px;
560
+ height: 6px;
561
+ border-radius: 3px;
562
+ background: linear-gradient(90deg, #1F2937 0%, #DC2626 35%, #F97316 65%, #FCD34D 100%);
563
+ box-shadow: 0 0 8px rgba(252, 211, 77, 0.25);
564
+ }
565
+ .tr-demo-cbar-caption {
566
+ position: absolute;
567
+ bottom: -1.1rem;
568
+ left: 0;
569
+ right: 0;
570
+ text-align: center;
571
+ font-size: 0.65rem;
572
+ color: var(--text-secondary);
573
+ text-transform: uppercase;
574
+ letter-spacing: 0.08em;
575
+ }
576
+
577
+ /* === Segmented controls (True / Compare / Predicted etc) ============ */
578
+ /* Restyle st.radio horizontal as a pill group. */
579
+
580
+ .tr-segmented div[role="radiogroup"] {
581
+ display: inline-flex;
582
+ background: rgba(255, 255, 255, 0.04);
583
+ border: 1px solid rgba(255, 255, 255, 0.08);
584
+ border-radius: 10px;
585
+ padding: 0.2rem;
586
+ gap: 0;
587
+ }
588
+ .tr-segmented div[role="radiogroup"] label {
589
+ margin: 0 !important;
590
+ padding: 0.4rem 1rem !important;
591
+ border-radius: 7px;
592
+ cursor: pointer;
593
+ color: var(--text-secondary) !important;
594
+ font-size: 0.82rem !important;
595
+ transition: all 0.2s ease;
596
+ background: transparent !important;
597
+ }
598
+ .tr-segmented div[role="radiogroup"] label:has(input:checked) {
599
+ background: rgba(255, 255, 255, 0.10) !important;
600
+ color: var(--text-primary) !important;
601
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06);
602
+ }
603
+ /* sr-only: hide visually but keep input in layout so BaseWeb onChange fires */
604
+ .tr-segmented div[role="radiogroup"] input {
605
+ position: absolute !important;
606
+ opacity: 0 !important;
607
+ width: 1px !important;
608
+ height: 1px !important;
609
+ margin: -1px !important;
610
+ padding: 0 !important;
611
+ overflow: hidden !important;
612
+ clip: rect(0, 0, 0, 0) !important;
613
+ border: 0 !important;
614
+ }
615
+ /* Hide the BaseWeb visual radio dot, but keep it click-targetable */
616
+ .tr-segmented div[role="radiogroup"] label > div:first-child {
617
+ width: 0 !important;
618
+ height: 0 !important;
619
+ overflow: hidden !important;
620
+ opacity: 0 !important;
621
+ margin: 0 !important;
622
+ padding: 0 !important;
623
+ border: 0 !important;
624
+ }
625
+ .tr-segmented div[role="radiogroup"] label > div:last-child p { margin: 0 !important; }
626
+
627
+ /* Tab strip: thin underline on the active tab */
628
+ .stTabs [data-baseweb="tab-list"] {
629
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
630
+ gap: 1.5rem;
631
+ }
632
+ .stTabs [data-baseweb="tab"] {
633
+ padding: 0.5rem 0;
634
+ color: var(--text-secondary);
635
+ font-size: 0.85rem;
636
+ background: transparent !important;
637
+ }
638
+ .stTabs [data-baseweb="tab"][aria-selected="true"] {
639
+ color: var(--text-primary);
640
+ border-bottom: 2px solid var(--accent-purple);
641
+ }
642
+
643
+ /* === TRIBE-style footer ============================================== */
644
+
645
+ .tr-foot {
646
+ margin: 4rem -1rem 0 -1rem;
647
+ padding: 3rem 2rem 1.5rem 2rem;
648
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
649
+ background: linear-gradient(180deg,
650
+ rgba(0, 0, 0, 0) 0%,
651
+ rgba(255, 255, 255, 0.015) 100%);
652
+ }
653
+ .tr-foot-grid {
654
+ display: grid;
655
+ grid-template-columns: 1.4fr 1fr 1fr 1fr;
656
+ gap: 2.5rem;
657
+ max-width: 1200px;
658
+ margin: 0 auto;
659
+ }
660
+ .tr-foot-brand-logo {
661
+ display: flex;
662
+ gap: 0.4rem;
663
+ margin-bottom: 0.7rem;
664
+ }
665
+ .tr-foot-dot {
666
+ width: 0.6rem;
667
+ height: 0.6rem;
668
+ border-radius: 50%;
669
+ display: inline-block;
670
+ }
671
+ .tr-foot-dot-a { background: var(--accent-purple); box-shadow: 0 0 8px var(--accent-purple); }
672
+ .tr-foot-dot-b { background: var(--accent-blue); box-shadow: 0 0 8px var(--accent-blue); }
673
+ .tr-foot-dot-c { background: var(--accent-cyan); box-shadow: 0 0 8px var(--accent-cyan); }
674
+ .tr-foot-brand-title {
675
+ font-size: 1.15rem;
676
+ font-weight: 700;
677
+ color: var(--text-primary);
678
+ letter-spacing: -0.01em;
679
+ margin-bottom: 0.25rem;
680
+ }
681
+ .tr-foot-brand-tagline {
682
+ color: var(--text-secondary);
683
+ font-size: 0.85rem;
684
+ line-height: 1.5;
685
+ margin-bottom: 1rem;
686
+ max-width: 18rem;
687
+ }
688
+ .tr-foot-built {
689
+ font-size: 0.78rem;
690
+ color: var(--text-secondary);
691
+ opacity: 0.8;
692
+ }
693
+ .tr-foot-built a {
694
+ color: var(--accent-purple);
695
+ text-decoration: none;
696
+ }
697
+ .tr-foot-col-title {
698
+ color: var(--text-primary);
699
+ font-size: 0.78rem;
700
+ font-weight: 600;
701
+ text-transform: uppercase;
702
+ letter-spacing: 0.1em;
703
+ margin-bottom: 0.9rem;
704
+ }
705
+ .tr-foot-list {
706
+ list-style: none;
707
+ padding: 0;
708
+ margin: 0;
709
+ display: flex;
710
+ flex-direction: column;
711
+ gap: 0.55rem;
712
+ }
713
+ .tr-foot-link {
714
+ color: var(--text-secondary);
715
+ text-decoration: none;
716
+ font-size: 0.85rem;
717
+ transition: color 0.2s ease;
718
+ }
719
+ .tr-foot-link:hover {
720
+ color: var(--text-primary);
721
+ }
722
+ .tr-foot-rule {
723
+ margin: 2.5rem auto 1rem auto;
724
+ max-width: 1200px;
725
+ border-top: 1px solid rgba(255, 255, 255, 0.07);
726
+ }
727
+ .tr-foot-legal {
728
+ max-width: 1200px;
729
+ margin: 0 auto;
730
+ display: flex;
731
+ justify-content: space-between;
732
+ align-items: center;
733
+ color: var(--text-secondary);
734
+ font-size: 0.72rem;
735
+ opacity: 0.7;
736
+ }
737
+ .tr-foot-legal-right {
738
+ display: flex;
739
+ gap: 0.6rem;
740
+ align-items: center;
741
+ }
742
+ .tr-foot-legal-right a {
743
+ color: var(--text-secondary);
744
+ text-decoration: none;
745
+ }
746
+ .tr-foot-legal-right a:hover { color: var(--text-primary); }
747
+
748
+ /* === Feature card grid (Analysis tools) ============================== */
749
+
750
+ .cl-feature-card {
751
+ --card-accent: #7C3AED;
752
+ background: rgba(15, 15, 35, 0.55);
753
+ backdrop-filter: blur(10px);
754
+ border: 1px solid color-mix(in srgb, var(--card-accent) 25%, transparent);
755
+ border-radius: 14px;
756
+ padding: 1.3rem 1.4rem 1.1rem 1.4rem;
757
+ transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
758
+ display: flex;
759
+ flex-direction: column;
760
+ min-height: 220px; /* uniform across the grid */
761
+ text-decoration: none;
762
+ }
763
+ .cl-feature-card:hover {
764
+ border-color: color-mix(in srgb, var(--card-accent) 70%, transparent);
765
+ box-shadow: 0 6px 24px color-mix(in srgb, var(--card-accent) 18%, transparent);
766
+ transform: translateY(-2px);
767
+ }
768
+
769
+ .cl-feature-icon {
770
+ font-size: 1.5rem;
771
+ margin-bottom: 0.6rem;
772
+ line-height: 1;
773
+ }
774
+ .cl-feature-title {
775
+ color: var(--card-accent);
776
+ font-size: 1.0rem;
777
+ font-weight: 600;
778
+ margin-bottom: 0.45rem;
779
+ letter-spacing: -0.005em;
780
+ }
781
+ .cl-feature-desc {
782
+ color: var(--text-secondary);
783
+ font-size: 0.83rem;
784
+ line-height: 1.55;
785
+ flex: 1; /* push CTA to the bottom of the card */
786
+ margin-bottom: 0.9rem;
787
+ }
788
+ .cl-feature-cta {
789
+ display: flex;
790
+ align-items: center;
791
+ justify-content: space-between;
792
+ color: var(--text-primary);
793
+ font-size: 0.82rem;
794
+ font-weight: 500;
795
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
796
+ padding-top: 0.75rem;
797
+ margin-top: auto;
798
+ transition: color 0.2s ease;
799
+ }
800
+ .cl-feature-arrow {
801
+ transition: transform 0.2s ease;
802
+ color: var(--card-accent);
803
+ font-weight: 600;
804
+ }
805
+ .cl-feature-card:hover .cl-feature-arrow {
806
+ transform: translateX(4px);
807
+ }
808
+ .cl-feature-link {
809
+ display: flex;
810
+ cursor: pointer;
811
+ color: inherit;
812
+ }
813
+ .cl-feature-link:visited { color: inherit; }
814
+
815
+
816
+ /* === Sidebar pre-hide (kill the initial flicker) ===================== */
817
+ /* Streamlit's collapsed sidebar still flashes briefly on first paint
818
+ * before its inline width animation finishes. Force the collapsed
819
+ * state at CSS level so users don't see the flash. The hamburger
820
+ * toggle stays clickable to expand it on demand. */
821
+
822
+ section[data-testid="stSidebar"][aria-expanded="false"] {
823
+ transform: translateX(-100%);
824
+ transition: transform 0.25s ease, width 0.25s ease;
825
+ visibility: hidden;
826
+ }
827
+ section[data-testid="stSidebar"][aria-expanded="true"] {
828
+ transform: translateX(0);
829
+ visibility: visible;
830
+ }
831
+
832
+ /* === Feature card refinements: anchor + spans + SVG icons =========== */
833
+
834
+ .cl-feature-card,
835
+ .cl-feature-card:visited,
836
+ .cl-feature-card:hover,
837
+ .cl-feature-card:focus {
838
+ text-decoration: none !important;
839
+ color: inherit;
840
+ }
841
+ .cl-feature-card span {
842
+ display: block;
843
+ text-decoration: none !important;
844
+ }
845
+ .cl-feature-card .cl-feature-cta-label,
846
+ .cl-feature-card .cl-feature-arrow {
847
+ display: inline-flex;
848
+ align-items: center;
849
+ }
850
+ /* Inline SVG icon block at the top of each card. */
851
+ .cl-feature-icon {
852
+ width: 2.0rem;
853
+ height: 2.0rem;
854
+ margin-bottom: 0.7rem;
855
+ color: var(--card-accent);
856
+ line-height: 1;
857
+ }
858
+ .cl-feature-icon svg {
859
+ width: 100%;
860
+ height: 100%;
861
+ display: block;
862
+ }
863
+ /* Arrow at bottom-right of the CTA strip; tints with the card accent. */
864
+ .cl-feature-arrow svg {
865
+ width: 1.05rem;
866
+ height: 1.05rem;
867
+ display: block;
868
+ }
869
+ .cl-feature-cta {
870
+ display: flex !important; /* override the .cl-feature-card span block rule */
871
+ align-items: center;
872
+ justify-content: space-between;
873
+ }
874
+
875
+ /* === Stat card (glow_card) and section header — variable-driven ===== */
876
+
877
+ .cl-stat-card {
878
+ --card-accent: #06B6D4;
879
+ background: var(--bg-glass);
880
+ backdrop-filter: blur(10px);
881
+ border: 1px solid color-mix(in srgb, var(--card-accent) 25%, var(--border-glass));
882
+ border-radius: 14px;
883
+ padding: 1.2rem 1rem;
884
+ text-align: center;
885
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
886
+ box-shadow: 0 0 22px color-mix(in srgb, var(--card-accent) 12%, transparent);
887
+ }
888
+ .cl-stat-card:hover {
889
+ border-color: color-mix(in srgb, var(--card-accent) 50%, var(--border-glass));
890
+ box-shadow: 0 0 32px color-mix(in srgb, var(--card-accent) 20%, transparent);
891
+ }
892
+ .cl-stat-label {
893
+ color: var(--text-secondary);
894
+ font-size: 0.72rem;
895
+ text-transform: uppercase;
896
+ letter-spacing: 0.1em;
897
+ margin-bottom: 0.4rem;
898
+ font-weight: 600;
899
+ }
900
+ .cl-stat-value {
901
+ color: var(--card-accent);
902
+ font-size: 2.0rem;
903
+ font-weight: 700;
904
+ letter-spacing: -0.02em;
905
+ line-height: 1.1;
906
+ }
907
+ .cl-stat-sub {
908
+ color: var(--text-secondary);
909
+ font-size: 0.74rem;
910
+ margin-top: 0.25rem;
911
+ opacity: 0.8;
912
+ }
913
+
914
+ .cl-section {
915
+ margin: 1.5rem 0 1rem 0;
916
+ }
917
+ .cl-section-title {
918
+ color: var(--text-primary);
919
+ font-size: 1.25rem;
920
+ font-weight: 600;
921
+ letter-spacing: -0.01em;
922
+ margin: 0 0 0.3rem 0;
923
+ padding-bottom: 0.4rem;
924
+ border-bottom: 1px solid var(--border-glass);
925
+ }
926
+ .cl-section-desc {
927
+ color: var(--text-secondary);
928
+ font-size: 0.85rem;
929
+ margin: 0;
930
+ }
931
+
932
+ /* The original .cl-feature-card relied on a hardcoded dark background.
933
+ * Switch it to the theme variable so the white overlay can re-skin it. */
934
+ .cl-feature-card {
935
+ background: var(--bg-glass);
936
+ }
937
+
938
+ /* === Theme toggle switch (sliding pill, sun/moon, in the header) ==== */
939
+
940
+ .tr-header-right {
941
+ display: flex;
942
+ align-items: center;
943
+ gap: 1.2rem;
944
+ justify-content: flex-end;
945
+ }
946
+ .tr-toggle {
947
+ text-decoration: none !important;
948
+ display: inline-flex;
949
+ align-items: center;
950
+ line-height: 1;
951
+ color: var(--text-primary);
952
+ }
953
+ .tr-toggle-track {
954
+ position: relative;
955
+ display: inline-flex;
956
+ align-items: center;
957
+ justify-content: space-between;
958
+ width: 3.4rem;
959
+ height: 1.6rem;
960
+ padding: 0 0.35rem;
961
+ background: rgba(255, 255, 255, 0.06);
962
+ border: 1px solid rgba(255, 255, 255, 0.14);
963
+ border-radius: 999px;
964
+ transition: background 0.25s ease, border-color 0.25s ease;
965
+ box-sizing: border-box;
966
+ }
967
+ .tr-toggle-track:hover {
968
+ border-color: rgba(255, 255, 255, 0.28);
969
+ }
970
+ .tr-toggle-side {
971
+ width: 0.95rem;
972
+ height: 0.95rem;
973
+ color: var(--text-secondary);
974
+ opacity: 0.55;
975
+ transition: opacity 0.2s ease, color 0.2s ease;
976
+ z-index: 0;
977
+ pointer-events: none;
978
+ }
979
+ .tr-toggle-icon {
980
+ width: 100%;
981
+ height: 100%;
982
+ display: block;
983
+ }
984
+ /* Active side glows; inactive side fades out */
985
+ .tr-toggle-track.off .tr-toggle-left {
986
+ opacity: 1;
987
+ color: #E2E8F0;
988
+ }
989
+ .tr-toggle-track.on .tr-toggle-right {
990
+ opacity: 1;
991
+ color: #FBBF24;
992
+ }
993
+ .tr-toggle-knob {
994
+ position: absolute;
995
+ top: 50%;
996
+ width: 1.25rem;
997
+ height: 1.25rem;
998
+ border-radius: 50%;
999
+ background: linear-gradient(180deg, #F8FAFC 0%, #CBD5E1 100%);
1000
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
1001
+ transform: translateY(-50%) translateX(0);
1002
+ transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
1003
+ z-index: 1;
1004
+ pointer-events: none;
1005
+ }
1006
+ .tr-toggle-track.off .tr-toggle-knob {
1007
+ transform: translateY(-50%) translateX(0);
1008
+ left: 0.18rem;
1009
+ }
1010
+ .tr-toggle-track.on .tr-toggle-knob {
1011
+ transform: translateY(-50%) translateX(1.78rem);
1012
+ left: 0.18rem;
1013
+ }
assets/theme_black.css ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* === Pure-black overlay ============================================== */
2
+ /* Overrides the default theme.css gradients with a flat #000000
3
+ * background. No purple wash, no radial glows. Text stays light. */
4
+
5
+ :root {
6
+ --bg-primary: #000000;
7
+ --bg-secondary: rgba(15, 15, 15, 0.85);
8
+ --bg-glass: rgba(20, 20, 20, 0.55);
9
+ --border-glass: rgba(255, 255, 255, 0.10);
10
+ --text-primary: #FFFFFF;
11
+ --text-secondary: #9CA3AF;
12
+ }
13
+
14
+ .stApp {
15
+ background: #000000 !important;
16
+ background-image: none !important;
17
+ }
18
+
19
+ section[data-testid="stSidebar"] {
20
+ background: #050505 !important;
21
+ border-right: 1px solid rgba(255, 255, 255, 0.06);
22
+ }
23
+
24
+ /* Soften the panel glow tint that the base theme bakes in. */
25
+ div[data-testid="stVerticalBlock"] > div[data-testid="stVerticalBlockBorderWrapper"] {
26
+ background: rgba(255, 255, 255, 0.02);
27
+ border: 1px solid rgba(255, 255, 255, 0.07);
28
+ box-shadow: none;
29
+ }
30
+
31
+ .tr-header {
32
+ background: linear-gradient(180deg, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.7) 100%);
33
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
34
+ }
35
+ .tr-demo-card {
36
+ background: rgba(15, 15, 15, 0.7);
37
+ border: 1px solid rgba(255, 255, 255, 0.08);
38
+ }
39
+ .tr-pipeline {
40
+ background: rgba(15, 15, 15, 0.55);
41
+ border: 1px solid rgba(255, 255, 255, 0.07);
42
+ }
43
+ .tr-pipe-source {
44
+ background: rgba(255, 255, 255, 0.04);
45
+ border-color: rgba(255, 255, 255, 0.10);
46
+ }
47
+ .tr-pipe-model {
48
+ background: rgba(255, 255, 255, 0.06);
49
+ border-color: rgba(255, 255, 255, 0.12);
50
+ color: #E5E7EB;
51
+ }
52
+ .tr-pipe-fuse {
53
+ background: rgba(255, 255, 255, 0.04);
54
+ border-color: rgba(255, 255, 255, 0.10);
55
+ color: #E5E7EB;
56
+ }
57
+ .tr-step-text u {
58
+ border-bottom-color: rgba(255, 255, 255, 0.35);
59
+ color: #E5E7EB;
60
+ }
assets/theme_white.css ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* === Pure-white overlay ============================================== */
2
+ /* Light-mode counterpart to theme_black.css. Pure #FFFFFF background,
3
+ * deep ink-on-paper text. No tints, no gradients. */
4
+
5
+ :root {
6
+ --bg-primary: #FFFFFF;
7
+ --bg-secondary: rgba(247, 247, 248, 0.95);
8
+ --bg-glass: rgba(248, 248, 250, 0.85);
9
+ --border-glass: rgba(0, 0, 0, 0.08);
10
+ --text-primary: #0F172A;
11
+ --text-secondary: #475569;
12
+ --accent-purple: #6D28D9;
13
+ --accent-blue: #2563EB;
14
+ --accent-cyan: #0891B2;
15
+ }
16
+
17
+ .stApp {
18
+ background: #FFFFFF !important;
19
+ background-image: none !important;
20
+ color: #0F172A;
21
+ }
22
+
23
+ section[data-testid="stSidebar"] {
24
+ background: #FAFAFA !important;
25
+ border-right: 1px solid rgba(0, 0, 0, 0.08);
26
+ }
27
+
28
+ div[data-testid="stVerticalBlock"] > div[data-testid="stVerticalBlockBorderWrapper"] {
29
+ background: #FFFFFF;
30
+ border: 1px solid rgba(0, 0, 0, 0.07);
31
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
32
+ }
33
+
34
+ div[data-testid="stExpander"] {
35
+ background: #FAFAFA;
36
+ border: 1px solid rgba(0, 0, 0, 0.08);
37
+ }
38
+
39
+ /* Body / paragraph text */
40
+ .stApp p, .stApp li, .stApp label, .stApp .stMarkdown {
41
+ color: #1F2937;
42
+ }
43
+
44
+ /* TRIBE-style header in light mode */
45
+ .tr-header {
46
+ background: rgba(255, 255, 255, 0.98);
47
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
48
+ }
49
+ .tr-header-title { color: #0F172A; }
50
+ .tr-header-subtitle { color: #6B7280; }
51
+ .tr-header-center { color: #6B7280; }
52
+ .tr-nav-link { color: #0F172A; }
53
+ .tr-arrow { opacity: 0.55; }
54
+
55
+ .tr-arch-title { color: #0F172A; }
56
+ .tr-arch-intro { color: #475569; }
57
+ .tr-step-title { color: #0F172A; }
58
+ .tr-step-text { color: #475569; }
59
+ .tr-step-num {
60
+ background: #F3F4F6;
61
+ border: 1px solid #D1D5DB;
62
+ color: #4B5563;
63
+ }
64
+ .tr-step-text u {
65
+ border-bottom-color: #6D28D9;
66
+ color: #5B21B6;
67
+ }
68
+
69
+ .tr-demo-card {
70
+ background: #FFFFFF;
71
+ border: 1px solid rgba(0, 0, 0, 0.08);
72
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
73
+ }
74
+ .tr-demo-title { color: #6B7280; }
75
+ .tr-demo-cbar-label, .tr-demo-cbar-caption { color: #6B7280; }
76
+
77
+ .tr-pipeline {
78
+ background: #FAFAFA;
79
+ border: 1px solid rgba(0, 0, 0, 0.07);
80
+ }
81
+ .tr-pipe-source {
82
+ background: #FFFFFF;
83
+ border-color: rgba(0, 0, 0, 0.10);
84
+ color: #0F172A;
85
+ }
86
+ .tr-pipe-model {
87
+ background: #EEF2FF;
88
+ border-color: #C7D2FE;
89
+ color: #3730A3;
90
+ }
91
+ .tr-pipe-fuse {
92
+ background: #FFFFFF;
93
+ border-color: rgba(0, 0, 0, 0.10);
94
+ color: #0F172A;
95
+ }
96
+ .tr-pipe-icon, .tr-pipe-arrow { color: #9CA3AF; }
97
+ .tr-pipe-icon-lg { color: #0F172A; }
98
+ .tr-pipe-label { color: #4B5563; }
99
+
100
+ /* Segmented control in light mode */
101
+ .tr-segmented div[role="radiogroup"] {
102
+ background: #F3F4F6;
103
+ border: 1px solid rgba(0, 0, 0, 0.08);
104
+ }
105
+ .tr-segmented div[role="radiogroup"] label {
106
+ color: #6B7280 !important;
107
+ }
108
+ .tr-segmented div[role="radiogroup"] label:has(input:checked) {
109
+ background: #FFFFFF !important;
110
+ color: #0F172A !important;
111
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
112
+ }
113
+
114
+ /* Tab strip */
115
+ .stTabs [data-baseweb="tab-list"] {
116
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
117
+ }
118
+ .stTabs [data-baseweb="tab"] {
119
+ color: #6B7280;
120
+ }
121
+ .stTabs [data-baseweb="tab"][aria-selected="true"] {
122
+ color: #0F172A;
123
+ border-bottom: 2px solid #0F172A;
124
+ }
125
+
126
+ /* Tables and code blocks */
127
+ .stApp table { color: #0F172A; }
128
+ .stApp th { background: #F9FAFB; color: #0F172A; }
129
+ .stApp td { background: #FFFFFF; color: #1F2937; }
130
+ .stApp code { background: #F3F4F6; color: #0F172A; border-radius: 4px; padding: 0.05rem 0.3rem; }
131
+
132
+ /* Buttons / page links */
133
+ .stApp a[data-testid="stPageLink"] {
134
+ background: rgba(0, 0, 0, 0.04);
135
+ border: 1px solid rgba(0, 0, 0, 0.08);
136
+ color: #0F172A !important;
137
+ }
138
+
139
+ /* Glow cards: drop the glow in light mode for clean look */
140
+ .stApp [style*="background: rgba(15, 15, 40"] {
141
+ /* Inline-styled glow cards render OK; nothing to override */
142
+ }
143
+
144
+ /* === Variable-driven helpers, light-mode tuning ====================== */
145
+
146
+ /* glow_card / stat card */
147
+ .cl-stat-card {
148
+ background: #FFFFFF;
149
+ border-color: color-mix(in srgb, var(--card-accent) 22%, rgba(0, 0, 0, 0.08));
150
+ box-shadow:
151
+ 0 1px 2px rgba(15, 23, 42, 0.05),
152
+ 0 0 14px color-mix(in srgb, var(--card-accent) 8%, transparent);
153
+ }
154
+ .cl-stat-card:hover {
155
+ border-color: color-mix(in srgb, var(--card-accent) 50%, rgba(0, 0, 0, 0.12));
156
+ box-shadow:
157
+ 0 4px 12px rgba(15, 23, 42, 0.07),
158
+ 0 0 22px color-mix(in srgb, var(--card-accent) 14%, transparent);
159
+ }
160
+ .cl-stat-label { color: #64748B; }
161
+ .cl-stat-sub { color: #64748B; }
162
+
163
+ /* section_header */
164
+ .cl-section-title {
165
+ color: #0F172A;
166
+ border-bottom-color: rgba(0, 0, 0, 0.08);
167
+ }
168
+ .cl-section-desc { color: #475569; }
169
+
170
+ /* Feature cards: the .cl-feature-card on light bg should be paper-white
171
+ * with a subtle border, not glassmorphic dark.
172
+ */
173
+ .cl-feature-card {
174
+ background: #FFFFFF;
175
+ border: 1px solid color-mix(in srgb, var(--card-accent) 22%, rgba(0, 0, 0, 0.08));
176
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
177
+ }
178
+ .cl-feature-card:hover {
179
+ border-color: color-mix(in srgb, var(--card-accent) 60%, rgba(0, 0, 0, 0.12));
180
+ box-shadow:
181
+ 0 8px 24px color-mix(in srgb, var(--card-accent) 15%, rgba(15, 23, 42, 0.08));
182
+ }
183
+ .cl-feature-desc { color: #475569; }
184
+ .cl-feature-cta {
185
+ color: #0F172A;
186
+ border-top-color: rgba(0, 0, 0, 0.08);
187
+ }
188
+
189
+ /* Streamlit's vertical-block wrappers can still have a hard dark
190
+ * background bleeding from the base theme. Force them transparent. */
191
+ div[data-testid="stVerticalBlock"] > div[data-testid="stVerticalBlockBorderWrapper"] {
192
+ background: transparent;
193
+ border: none;
194
+ box-shadow: none;
195
+ padding: 0;
196
+ }
197
+
198
+ /* Plotly chart wrappers */
199
+ div[data-testid="stPlotlyChart"] {
200
+ background: #FFFFFF;
201
+ border: 1px solid rgba(0, 0, 0, 0.08);
202
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
203
+ }
204
+
205
+ /* Sidebar text */
206
+ section[data-testid="stSidebar"] * {
207
+ color: #0F172A;
208
+ }
209
+ section[data-testid="stSidebar"] .stMarkdown h1,
210
+ section[data-testid="stSidebar"] .stMarkdown h2,
211
+ section[data-testid="stSidebar"] .stMarkdown h3 {
212
+ color: #1E40AF;
213
+ }
214
+
215
+ /* Hide the radial-gradient bleed-through more aggressively. */
216
+ .stApp,
217
+ .stApp > div,
218
+ [data-testid="stAppViewContainer"],
219
+ [data-testid="stMain"],
220
+ [data-testid="stMain"] > div {
221
+ background: #FFFFFF !important;
222
+ background-image: none !important;
223
+ }
224
+
225
+ /* === Theme toggle switch (light-mode tuning) ========================= */
226
+ .tr-toggle-track {
227
+ background: #F1F5F9;
228
+ border-color: rgba(15, 23, 42, 0.15);
229
+ }
230
+ .tr-toggle-track:hover {
231
+ border-color: rgba(15, 23, 42, 0.3);
232
+ }
233
+ .tr-toggle-side {
234
+ color: #64748B;
235
+ }
236
+ .tr-toggle-track.off .tr-toggle-left {
237
+ color: #1E293B;
238
+ }
239
+ .tr-toggle-track.on .tr-toggle-right {
240
+ color: #F59E0B;
241
+ }
242
+ .tr-toggle-knob {
243
+ background: linear-gradient(180deg, #FFFFFF 0%, #E2E8F0 100%);
244
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.18);
245
+ }
brain_mesh.py CHANGED
@@ -55,14 +55,31 @@ ACTIVATION_PATTERNS = {
55
  # --- Mesh Loading ---
56
 
57
  @st.cache_resource
58
- def load_fsaverage_mesh(hemi="left", resolution="fsaverage5"):
59
- """Load fsaverage brain mesh via nilearn. Returns (coords, faces)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  from nilearn.datasets import fetch_surf_fsaverage
61
  from nilearn.surface import load_surf_mesh
62
 
63
  fsaverage = fetch_surf_fsaverage(mesh=resolution)
64
- key = f"pial_{hemi}"
65
- coords, faces = load_surf_mesh(fsaverage[key])
 
 
 
 
66
  return np.array(coords, dtype=np.float32), np.array(faces, dtype=np.int32)
67
 
68
 
@@ -125,7 +142,14 @@ def blend_with_sulcal(vertex_data, sulcal_map, data_opacity=0.85):
125
  # --- Plotly Rendering ---
126
 
127
  def _make_mesh3d(coords, faces, vertex_data, cmap, vmin, vmax, opacity=1.0, name=""):
128
- """Create a Plotly Mesh3d trace."""
 
 
 
 
 
 
 
129
  return go.Mesh3d(
130
  x=coords[:, 0], y=coords[:, 1], z=coords[:, 2],
131
  i=faces[:, 0], j=faces[:, 1], k=faces[:, 2],
@@ -134,14 +158,50 @@ def _make_mesh3d(coords, faces, vertex_data, cmap, vmin, vmax, opacity=1.0, name
134
  colorscale=cmap,
135
  cmin=vmin, cmax=vmax,
136
  opacity=opacity,
137
- lighting=dict(ambient=0.4, diffuse=0.6, specular=0.3, roughness=0.5, fresnel=0.2),
138
- lightposition=dict(x=100, y=200, z=300),
 
 
 
 
 
 
139
  showscale=False,
140
  name=name,
141
  hovertemplate="Vertex: %{pointNumber}<br>Value: %{intensity:.3f}<extra></extra>",
142
  )
143
 
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  def _scene_layout(camera, bg_color="#0E1117"):
146
  """Create a Plotly 3D scene layout."""
147
  return dict(
@@ -235,9 +295,19 @@ def _render_pyvista(coords, faces, vertex_data, cmap, vmin, vmax,
235
  mesh = pv.PolyData(coords, pv_faces)
236
  mesh.point_data["activation"] = vertex_data
237
 
238
- cmap_map = {"Hot": "hot", "Inferno": "inferno", "Plasma": "plasma",
239
- "Viridis": "viridis", "RdBu_r": "RdBu_r", "Coolwarm": "coolwarm"}
240
- pv_cmap = cmap_map.get(cmap, "hot")
 
 
 
 
 
 
 
 
 
 
241
 
242
  plotter = pv.Plotter(window_size=[900, 600], off_screen=True)
243
  plotter.add_mesh(
 
55
  # --- Mesh Loading ---
56
 
57
  @st.cache_resource
58
+ def load_fsaverage_mesh(hemi="left", resolution="fsaverage5", surface="pial"):
59
+ """Load an fsaverage brain mesh via nilearn.
60
+
61
+ Parameters
62
+ ----------
63
+ hemi
64
+ ``"left"`` or ``"right"``.
65
+ resolution
66
+ ``"fsaverage5"``, ``"fsaverage6"``, ``"fsaverage7"``, or
67
+ ``"fsaverage"`` (= 7).
68
+ surface
69
+ ``"pial"`` (real cortical surface, default), ``"inflated"``
70
+ (smooth balloon used in encoding papers), or ``"white"``
71
+ (gray/white-matter boundary).
72
+ """
73
  from nilearn.datasets import fetch_surf_fsaverage
74
  from nilearn.surface import load_surf_mesh
75
 
76
  fsaverage = fetch_surf_fsaverage(mesh=resolution)
77
+ surface_key = {
78
+ "pial": f"pial_{hemi}",
79
+ "inflated": f"infl_{hemi}",
80
+ "white": f"white_{hemi}",
81
+ }.get(surface, f"pial_{hemi}")
82
+ coords, faces = load_surf_mesh(fsaverage[surface_key])
83
  return np.array(coords, dtype=np.float32), np.array(faces, dtype=np.int32)
84
 
85
 
 
142
  # --- Plotly Rendering ---
143
 
144
  def _make_mesh3d(coords, faces, vertex_data, cmap, vmin, vmax, opacity=1.0, name=""):
145
+ """Create a Plotly Mesh3d trace.
146
+
147
+ Lighting tuned for an inflated cortex: low ambient (so concavities
148
+ actually look concave), strong diffuse, modest specular for the
149
+ Phong highlight on gyri. ``cmap`` may be either a Plotly named
150
+ colormap (``"Hot"``, ``"Inferno"``) or a list of ``[stop, color]``
151
+ pairs, e.g. for a custom hot-on-gray cortex palette.
152
+ """
153
  return go.Mesh3d(
154
  x=coords[:, 0], y=coords[:, 1], z=coords[:, 2],
155
  i=faces[:, 0], j=faces[:, 1], k=faces[:, 2],
 
158
  colorscale=cmap,
159
  cmin=vmin, cmax=vmax,
160
  opacity=opacity,
161
+ lighting=dict(
162
+ ambient=0.55, # bumped so the cortex base color reads correctly
163
+ diffuse=0.85, # main shape modulation from the directional light
164
+ specular=0.55, # Phong highlight; gives gyri a soft sheen
165
+ roughness=0.45, # slightly polished, not matte
166
+ fresnel=0.15, # subtle rim brightening
167
+ ),
168
+ lightposition=dict(x=120, y=200, z=400),
169
  showscale=False,
170
  name=name,
171
  hovertemplate="Vertex: %{pointNumber}<br>Value: %{intensity:.3f}<extra></extra>",
172
  )
173
 
174
 
175
+ def make_hot_on_cortex_colorscale(theme_mode: str = "black") -> list[list]:
176
+ """Return a Plotly colorscale that keeps the cortex visible.
177
+
178
+ Pure ``"Hot"`` maps zero to black, so non-activated cortex
179
+ disappears against a black background. This palette pins the low
180
+ end to a neutral gray (light enough to read on dark, dark enough
181
+ to read on light), holds it for a small plateau, then ramps
182
+ through dark-red → red → orange → gold → white at the high end.
183
+
184
+ Returned format matches Plotly's colorscale list spec:
185
+ ``[[stop, "#hex"], ...]``.
186
+ """
187
+ if theme_mode == "white":
188
+ cortex = "#9CA3AF" # slate-400, reads clearly on #FFFFFF
189
+ plateau_end = "#9CA3AF"
190
+ else:
191
+ cortex = "#3F3F46" # zinc-700, reads clearly on #000000
192
+ plateau_end = "#3F3F46"
193
+ # Hot ramp on top of the cortex plateau.
194
+ return [
195
+ [0.00, cortex],
196
+ [0.12, plateau_end], # plateau: sub-threshold cortex stays gray
197
+ [0.20, "#7F1D1D"], # dark red (subtle onset)
198
+ [0.40, "#DC2626"], # red
199
+ [0.62, "#F97316"], # orange
200
+ [0.82, "#FBBF24"], # gold
201
+ [1.00, "#FFFFFF"], # white peak
202
+ ]
203
+
204
+
205
  def _scene_layout(camera, bg_color="#0E1117"):
206
  """Create a Plotly 3D scene layout."""
207
  return dict(
 
295
  mesh = pv.PolyData(coords, pv_faces)
296
  mesh.point_data["activation"] = vertex_data
297
 
298
+ # The Plotly path accepts a list of [stop, hex] pairs as a custom
299
+ # colorscale; convert that to a matplotlib LinearSegmentedColormap
300
+ # so PyVista can use it. Named colormaps fall through unchanged.
301
+ if isinstance(cmap, list):
302
+ from matplotlib.colors import LinearSegmentedColormap
303
+ pv_cmap = LinearSegmentedColormap.from_list(
304
+ "cl_custom",
305
+ [(stop, color) for stop, color in cmap],
306
+ )
307
+ else:
308
+ cmap_map = {"Hot": "hot", "Inferno": "inferno", "Plasma": "plasma",
309
+ "Viridis": "viridis", "RdBu_r": "RdBu_r", "Coolwarm": "coolwarm"}
310
+ pv_cmap = cmap_map.get(cmap, "hot")
311
 
312
  plotter = pv.Plotter(window_size=[900, 600], off_screen=True)
313
  plotter.add_mesh(
theme.py CHANGED
@@ -4,13 +4,37 @@ import streamlit as st
4
  from pathlib import Path
5
 
6
 
7
- def inject_theme():
8
- """Inject custom CSS and hide default Streamlit chrome."""
9
- css_path = Path(__file__).parent / "assets" / "theme.css"
10
- if css_path.exists():
11
- css = css_path.read_text()
12
- else:
13
- css = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  st.markdown(f"<style>{css}</style>", unsafe_allow_html=True)
16
 
@@ -74,58 +98,155 @@ def hero_header(title, subtitle="", github_url="https://github.com/siddhant-rajh
74
 
75
 
76
  def glow_card(title, value, subtitle="", color="#06B6D4"):
77
- """Render a glowing metric card."""
78
- st.markdown(f"""
79
- <div style="
80
- background: rgba(15, 15, 40, 0.5);
81
- backdrop-filter: blur(10px);
82
- border: 1px solid {color}33;
83
- border-radius: 14px;
84
- padding: 1.2rem;
85
- text-align: center;
86
- box-shadow: 0 0 20px {color}15;
87
- transition: all 0.3s ease;
88
- ">
89
- <div style="color: #94A3B8; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 0.3rem;">{title}</div>
90
- <div style="color: {color}; font-size: 2rem; font-weight: 700; letter-spacing: -0.02em;">{value}</div>
91
- <div style="color: #64748B; font-size: 0.75rem; margin-top: 0.2rem;">{subtitle}</div>
92
- </div>
93
- """, unsafe_allow_html=True)
94
 
95
 
96
  def section_header(title, description=""):
97
- """Render a styled section header with optional description."""
98
- st.markdown(f"""
99
- <div style="margin: 1.5rem 0 0.8rem 0;">
100
- <h2 style="
101
- color: #F1F5F9;
102
- font-size: 1.3rem;
103
- font-weight: 600;
104
- letter-spacing: -0.02em;
105
- margin-bottom: 0.3rem;
106
- padding-bottom: 0.4rem;
107
- border-bottom: 1px solid rgba(100, 100, 255, 0.15);
108
- ">{title}</h2>
109
- {"<p style='color: #94A3B8; font-size: 0.85rem; margin-top: 0;'>" + description + "</p>" if description else ""}
110
- </div>
111
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
 
114
  def feature_card(icon, title, description, color="#7C3AED"):
115
- """Render a feature card for the home page."""
 
 
 
 
 
 
 
 
 
116
  return f"""
117
- <div style="
118
- background: rgba(15, 15, 40, 0.4);
119
- backdrop-filter: blur(8px);
120
- border: 1px solid {color}25;
121
- border-radius: 14px;
122
- padding: 1.3rem;
123
- height: 100%;
124
- transition: all 0.3s ease;
125
- cursor: pointer;
126
- ">
127
- <div style="font-size: 1.5rem; margin-bottom: 0.5rem;">{icon}</div>
128
- <div style="color: {color}; font-size: 1rem; font-weight: 600; margin-bottom: 0.4rem;">{title}</div>
129
- <div style="color: #94A3B8; font-size: 0.8rem; line-height: 1.5;">{description}</div>
130
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  """
 
4
  from pathlib import Path
5
 
6
 
7
+ THEME_MODES = ("black", "white")
8
+
9
+
10
+ def get_theme_mode() -> str:
11
+ """Read the active theme from session state. Defaults to ``"black"``."""
12
+ mode = st.session_state.get("ui_theme_mode", "black")
13
+ return mode if mode in THEME_MODES else "black"
14
+
15
+
16
+ def set_theme_mode(mode: str) -> None:
17
+ """Set the active theme. Use ``"black"`` for pure-black background,
18
+ ``"white"`` for pure-white background. Anything else is normalized."""
19
+ st.session_state["ui_theme_mode"] = mode if mode in THEME_MODES else "black"
20
+
21
+
22
+ def inject_theme(mode: str | None = None):
23
+ """Inject custom CSS for the chosen theme and hide Streamlit chrome.
24
+
25
+ When ``mode`` is ``None`` we read the active mode from session state
26
+ via :func:`get_theme_mode`. The base ``assets/theme.css`` is always
27
+ included; theme-specific overrides are appended on top.
28
+ """
29
+ if mode is None:
30
+ mode = get_theme_mode()
31
+
32
+ base_css_path = Path(__file__).parent / "assets" / "theme.css"
33
+ overlay_css_path = Path(__file__).parent / "assets" / f"theme_{mode}.css"
34
+
35
+ css = base_css_path.read_text() if base_css_path.exists() else ""
36
+ if overlay_css_path.exists():
37
+ css += "\n\n" + overlay_css_path.read_text()
38
 
39
  st.markdown(f"<style>{css}</style>", unsafe_allow_html=True)
40
 
 
98
 
99
 
100
  def glow_card(title, value, subtitle="", color="#06B6D4"):
101
+ """Render a glowing metric card.
102
+
103
+ Layout uses the ``cl-stat-card`` CSS class so background / border /
104
+ text colors come from theme variables and flip automatically when
105
+ the user switches between Black and White modes. Only the accent
106
+ color (the big number) stays per-card.
107
+ """
108
+ st.markdown(
109
+ f"""
110
+ <div class="cl-stat-card" style="--card-accent: {color};">
111
+ <div class="cl-stat-label">{title}</div>
112
+ <div class="cl-stat-value">{value}</div>
113
+ <div class="cl-stat-sub">{subtitle}</div>
114
+ </div>
115
+ """,
116
+ unsafe_allow_html=True,
117
+ )
118
 
119
 
120
  def section_header(title, description=""):
121
+ """Render a styled section header with optional description.
122
+
123
+ Colors come from theme variables (``--text-primary`` for the title,
124
+ ``--text-secondary`` for the description, ``--border-glass`` for
125
+ the underline) so the same markup reads correctly on dark and
126
+ light backgrounds.
127
+ """
128
+ desc_html = (
129
+ f'<p class="cl-section-desc">{description}</p>' if description else ""
130
+ )
131
+ st.markdown(
132
+ f"""
133
+ <div class="cl-section">
134
+ <h2 class="cl-section-title">{title}</h2>
135
+ {desc_html}
136
+ </div>
137
+ """,
138
+ unsafe_allow_html=True,
139
+ )
140
+
141
+
142
+ _FEATURE_ICONS: dict[str, str] = {
143
+ # Heroicons-style monochrome strokes; small currentColor SVGs so the
144
+ # accent color flows from the parent `--card-accent` variable.
145
+ "target": (
146
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" '
147
+ 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
148
+ '<circle cx="12" cy="12" r="9"/>'
149
+ '<circle cx="12" cy="12" r="5"/>'
150
+ '<circle cx="12" cy="12" r="1.5" fill="currentColor"/>'
151
+ '</svg>'
152
+ ),
153
+ "bars": (
154
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" '
155
+ 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
156
+ '<path d="M5 21V11"/><path d="M12 21V4"/><path d="M19 21V14"/>'
157
+ '<path d="M3 21h18"/></svg>'
158
+ ),
159
+ "clock": (
160
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" '
161
+ 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
162
+ '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2.5"/></svg>'
163
+ ),
164
+ "graph": (
165
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" '
166
+ 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
167
+ '<circle cx="6" cy="7" r="2.2"/><circle cx="18" cy="7" r="2.2"/>'
168
+ '<circle cx="6" cy="17" r="2.2"/><circle cx="18" cy="17" r="2.2"/>'
169
+ '<circle cx="12" cy="12" r="2.4"/>'
170
+ '<path d="M8 7h2.5M13.5 7H16M8 17h2.5M13.5 17H16M6 9.2v5.6M18 9.2v5.6"/></svg>'
171
+ ),
172
+ "brain": (
173
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" '
174
+ 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
175
+ '<path d="M9 5a3 3 0 0 0-3 3v0a2.5 2.5 0 0 0-2 4 2.5 2.5 0 0 0 1 4.5A3 3 0 0 0 9 19V5z"/>'
176
+ '<path d="M15 5a3 3 0 0 1 3 3v0a2.5 2.5 0 0 1 2 4 2.5 2.5 0 0 1-1 4.5A3 3 0 0 1 15 19V5z"/>'
177
+ '<path d="M12 5v14"/></svg>'
178
+ ),
179
+ "broadcast": (
180
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" '
181
+ 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
182
+ '<circle cx="12" cy="12" r="2.5" fill="currentColor"/>'
183
+ '<path d="M8.5 8.5a5 5 0 0 0 0 7"/><path d="M15.5 15.5a5 5 0 0 0 0-7"/>'
184
+ '<path d="M5.5 5.5a9 9 0 0 0 0 13"/><path d="M18.5 18.5a9 9 0 0 0 0-13"/></svg>'
185
+ ),
186
+ "arrow": (
187
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" '
188
+ 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
189
+ '<path d="M5 12h14"/><path d="M13 6l6 6-6 6"/></svg>'
190
+ ),
191
+ }
192
+
193
+
194
+ def feature_icon(name: str) -> str:
195
+ """Return inline SVG markup for a named card icon. Unknown names
196
+ fall back to a hollow circle so layout still works."""
197
+ return _FEATURE_ICONS.get(
198
+ name,
199
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" '
200
+ 'stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="9"/></svg>',
201
+ )
202
 
203
 
204
  def feature_card(icon, title, description, color="#7C3AED"):
205
+ """Render a feature card for the home page (no inline CTA).
206
+
207
+ Kept for backwards compatibility. New callers should prefer
208
+ :func:`feature_card_link` which produces a uniform-height card
209
+ with a built-in "Open ..." link, so a row of cards lines up.
210
+
211
+ ``icon`` may be either a name registered in ``_FEATURE_ICONS``
212
+ (e.g. ``"target"``) or raw SVG / HTML markup.
213
+ """
214
+ icon_html = _FEATURE_ICONS.get(icon, icon)
215
  return f"""
216
+ <span class="cl-feature-card" style="--card-accent: {color};">
217
+ <span class="cl-feature-icon">{icon_html}</span>
218
+ <span class="cl-feature-title">{title}</span>
219
+ <span class="cl-feature-desc">{description}</span>
220
+ </span>
221
+ """
222
+
223
+
224
+ def feature_card_link(icon, title, description, href: str, color="#7C3AED"):
225
+ """Render a feature card as a single clickable anchor element.
226
+
227
+ Inner blocks are rendered as ``<span>`` elements with
228
+ ``display: block`` in CSS anchors only allow phrasing-content
229
+ children in HTML5, and Streamlit's HTML sanitizer hoists ``<div>``
230
+ children out of ``<a>``, splitting the card visually. Spans are
231
+ safe.
232
+
233
+ ``icon`` may be either a name registered in ``_FEATURE_ICONS``
234
+ (``"target"``, ``"bars"``, ``"clock"``, ``"graph"``, ``"brain"``,
235
+ ``"broadcast"``) or raw SVG markup.
236
+ ``href`` is the multipage URL Streamlit exposes (e.g.
237
+ ``./Brain_Alignment``).
238
+ """
239
+ icon_html = _FEATURE_ICONS.get(icon, icon)
240
+ arrow = _FEATURE_ICONS["arrow"]
241
+ return f"""
242
+ <a class="cl-feature-card cl-feature-link" href="{href}"
243
+ style="--card-accent: {color};" target="_self">
244
+ <span class="cl-feature-icon">{icon_html}</span>
245
+ <span class="cl-feature-title">{title}</span>
246
+ <span class="cl-feature-desc">{description}</span>
247
+ <span class="cl-feature-cta">
248
+ <span class="cl-feature-cta-label">Open</span>
249
+ <span class="cl-feature-arrow">{arrow}</span>
250
+ </span>
251
+ </a>
252
  """
tribe.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TRIBE v2 demo-styled UI components.
2
+
3
+ Mirrors the visual structure of `aidemos.atmeta.com/tribev2`:
4
+
5
+ * A branded top header with the project name on the left, parent
6
+ attribution centered, and navigation links on the right.
7
+ * A two-column hero with the architecture explanation (numbered
8
+ steps + flow diagram) on the left and the interactive demo
9
+ (3D brain + controls + tabs + video) on the right.
10
+ * Segmented toggle controls (True / Compare / Predicted, etc.)
11
+ styled as pill groups.
12
+ * Tab strip with thin underline indicator.
13
+
14
+ All components are pure HTML+CSS injected via ``st.markdown``;
15
+ no third-party Streamlit components needed. The CSS classes are
16
+ prefixed ``tr-`` so they don't collide with the existing theme.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import streamlit as st
22
+
23
+
24
+ def _consume_theme_query_param() -> None:
25
+ """Apply ``?theme=black|white`` from the URL, then strip it.
26
+
27
+ The HTML toggle renders an anchor with ``href=?theme=<next>``;
28
+ clicking it triggers a navigation that Streamlit converts into
29
+ a rerun with ``st.query_params["theme"]`` set. We honor it here
30
+ and clear the param so the URL stays clean for sharing.
31
+ """
32
+ from theme import get_theme_mode, set_theme_mode # local: avoid cycle
33
+ qp = st.query_params
34
+ requested = qp.get("theme")
35
+ if requested in ("black", "white") and requested != get_theme_mode():
36
+ set_theme_mode(requested)
37
+ try:
38
+ del qp["theme"]
39
+ except KeyError:
40
+ pass
41
+ st.rerun()
42
+
43
+
44
+ def top_header(
45
+ title: str = "CortexLab",
46
+ subtitle: str = "AI research toolkit",
47
+ parent: str = "Stevens · Meta TRIBE v2",
48
+ links: list[tuple[str, str]] | None = None,
49
+ theme_toggle: bool = True,
50
+ ) -> None:
51
+ """Render the TRIBE-style top header bar.
52
+
53
+ Layout: title block on the far left, parent attribution centered,
54
+ a sliding sun/moon toggle and a row of text links on the far right.
55
+ The toggle is rendered as an inline anchor with a CSS-only sliding
56
+ pill; clicking it navigates to ``?theme=<next>``, which Streamlit
57
+ serves as a rerun. :func:`_consume_theme_query_param` (called at
58
+ the top of the script) then applies the new mode and strips the
59
+ query param.
60
+ """
61
+ from theme import get_theme_mode # local: keep theme import deferred
62
+
63
+ if theme_toggle:
64
+ _consume_theme_query_param()
65
+
66
+ if links is None:
67
+ links = [
68
+ ("Code", "https://github.com/siddhant-rajhans/cortexlab"),
69
+ ("Demo", "https://huggingface.co/spaces/SID2000/cortexlab-dashboard"),
70
+ ("Paper", "https://github.com/siddhant-rajhans/cortexlab"),
71
+ ("Hub", "https://huggingface.co/SID2000/cortexlab"),
72
+ ]
73
+ link_html = " ".join(
74
+ f'<a class="tr-nav-link" href="{href}" target="_blank">{label} <span class="tr-arrow">↗</span></a>'
75
+ for label, href in links
76
+ )
77
+
78
+ toggle_html = ""
79
+ if theme_toggle:
80
+ cur_mode = get_theme_mode()
81
+ next_mode = "white" if cur_mode == "black" else "black"
82
+ is_on = cur_mode == "white"
83
+ sun = (
84
+ '<svg class="tr-toggle-icon" viewBox="0 0 24 24" fill="none" '
85
+ 'stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
86
+ 'stroke-linejoin="round" aria-hidden="true">'
87
+ '<circle cx="12" cy="12" r="4.2"/>'
88
+ '<path d="M12 3v2"/><path d="M12 19v2"/>'
89
+ '<path d="M3 12h2"/><path d="M19 12h2"/>'
90
+ '<path d="M5.6 5.6l1.4 1.4"/><path d="M17 17l1.4 1.4"/>'
91
+ '<path d="M5.6 18.4l1.4-1.4"/><path d="M17 7l1.4-1.4"/></svg>'
92
+ )
93
+ moon = (
94
+ '<svg class="tr-toggle-icon" viewBox="0 0 24 24" fill="none" '
95
+ 'stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
96
+ 'stroke-linejoin="round" aria-hidden="true">'
97
+ '<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>'
98
+ )
99
+ toggle_html = (
100
+ f'<a class="tr-toggle" href="?theme={next_mode}" target="_self" '
101
+ f'role="switch" aria-checked="{str(is_on).lower()}" '
102
+ 'aria-label="Toggle light/dark theme">'
103
+ f'<span class="tr-toggle-track {"on" if is_on else "off"}">'
104
+ f' <span class="tr-toggle-side tr-toggle-left">{moon}</span>'
105
+ f' <span class="tr-toggle-side tr-toggle-right">{sun}</span>'
106
+ f' <span class="tr-toggle-knob"></span>'
107
+ f'</span>'
108
+ '</a>'
109
+ )
110
+
111
+ st.markdown(
112
+ f"""
113
+ <div class="tr-header">
114
+ <div class="tr-header-left">
115
+ <div class="tr-header-title">{title}</div>
116
+ <div class="tr-header-subtitle">{subtitle}</div>
117
+ </div>
118
+ <div class="tr-header-center">{parent}</div>
119
+ <div class="tr-header-right">{toggle_html}{link_html}</div>
120
+ </div>
121
+ """,
122
+ unsafe_allow_html=True,
123
+ )
124
+
125
+
126
+ def architecture_steps(
127
+ title: str = "CortexLab: a multimodal encoding pipeline",
128
+ intro: str = "CortexLab predicts brain activity through a three-stage pipeline:",
129
+ steps: list[tuple[str, str]] | None = None,
130
+ ) -> None:
131
+ """Numbered architecture description card.
132
+
133
+ ``steps`` is a list of (heading, body_html) tuples. The body may
134
+ include inline markup (e.g. underlined modality names).
135
+ """
136
+ if steps is None:
137
+ steps = [
138
+ (
139
+ "Multimodal feature extraction",
140
+ "Pretrained CLIP, V-JEPA 2, and CLIP-text encoders extract <u>vision</u>, <u>motion</u>, "
141
+ "and <u>language</u> embeddings from short video clips and machine-generated captions.",
142
+ ),
143
+ (
144
+ "Voxelwise ridge encoding",
145
+ "A fused Triton kernel solves <u>327,684</u> independent voxelwise ridge regressions in seconds, "
146
+ "mapping each modality's features to per-voxel BOLD responses on the fsaverage cortical surface.",
147
+ ),
148
+ (
149
+ "Causal modality lesion",
150
+ "Each modality is ablated at test time and per-voxel ΔR² is permutation-tested across "
151
+ "1,000 shuffles, BH-FDR corrected, and rendered onto the inflated cortex.",
152
+ ),
153
+ ]
154
+ items_html = "".join(
155
+ f"""
156
+ <li class="tr-step">
157
+ <span class="tr-step-num">{i}</span>
158
+ <div class="tr-step-body">
159
+ <strong class="tr-step-title">{title_}</strong>
160
+ <p class="tr-step-text">{body}</p>
161
+ </div>
162
+ </li>
163
+ """
164
+ for i, (title_, body) in enumerate(steps, start=1)
165
+ )
166
+ st.markdown(
167
+ f"""
168
+ <div class="tr-arch">
169
+ <h2 class="tr-arch-title">{title}</h2>
170
+ <p class="tr-arch-intro">{intro}</p>
171
+ <ul class="tr-step-list">{items_html}</ul>
172
+ </div>
173
+ """,
174
+ unsafe_allow_html=True,
175
+ )
176
+
177
+
178
+ def pipeline_diagram() -> None:
179
+ """Flow diagram: stimulus rows → per-modality encoders → fusion → brain.
180
+
181
+ Mirrors TRIBE's "Video → V-JEPA2", "Audio → wav2vec 2.0",
182
+ "Text → Llama 3.2" diagram visually but with our model lineup.
183
+ """
184
+ st.markdown(
185
+ """
186
+ <div class="tr-pipeline">
187
+ <div class="tr-pipe-rows">
188
+ <div class="tr-pipe-row">
189
+ <div class="tr-pipe-source"><span class="tr-pipe-icon">▶</span> Video frame</div>
190
+ <span class="tr-pipe-arrow">→</span>
191
+ <div class="tr-pipe-model">CLIP ViT-L/14</div>
192
+ </div>
193
+ <div class="tr-pipe-row">
194
+ <div class="tr-pipe-source"><span class="tr-pipe-icon">▤</span> Motion</div>
195
+ <span class="tr-pipe-arrow">→</span>
196
+ <div class="tr-pipe-model">V-JEPA 2</div>
197
+ </div>
198
+ <div class="tr-pipe-row">
199
+ <div class="tr-pipe-source"><span class="tr-pipe-icon">T</span> Caption</div>
200
+ <span class="tr-pipe-arrow">→</span>
201
+ <div class="tr-pipe-model">CLIP text</div>
202
+ </div>
203
+ </div>
204
+ <span class="tr-pipe-arrow tr-pipe-arrow-h">→</span>
205
+ <div class="tr-pipe-fuse">
206
+ <div class="tr-pipe-icon-lg">⚙</div>
207
+ <div class="tr-pipe-label">Voxelwise<br/>ridge</div>
208
+ </div>
209
+ <span class="tr-pipe-arrow tr-pipe-arrow-h">→</span>
210
+ <div class="tr-pipe-fuse">
211
+ <div class="tr-pipe-icon-lg">𓆑</div>
212
+ <div class="tr-pipe-label">Subject<br/>block</div>
213
+ </div>
214
+ </div>
215
+ """,
216
+ unsafe_allow_html=True,
217
+ )
218
+
219
+
220
+ def segmented(
221
+ label: str,
222
+ options: list[str],
223
+ *,
224
+ default_index: int = 0,
225
+ key: str | None = None,
226
+ ) -> str:
227
+ """Pill-style segmented control. Returns the selected value.
228
+
229
+ Built on top of ``st.radio`` with custom CSS that hides the radio
230
+ dots and styles each option as a pill. The active option gets a
231
+ subtle background and white text; inactive options stay muted.
232
+ """
233
+ return st.radio(
234
+ label,
235
+ options,
236
+ index=default_index,
237
+ horizontal=True,
238
+ key=key,
239
+ label_visibility="collapsed",
240
+ )
241
+
242
+
243
+ def demo_card_open(title: str = "Live encoder · sample stimulus") -> None:
244
+ """Open the rounded card that wraps the demo column. Pair with
245
+ :func:`demo_card_close`."""
246
+ st.markdown(
247
+ f"""
248
+ <div class="tr-demo-card">
249
+ <div class="tr-demo-header">
250
+ <span class="tr-demo-title">{title}</span>
251
+ <span class="tr-demo-cbar">
252
+ <span class="tr-demo-cbar-label">Low</span>
253
+ <span class="tr-demo-cbar-grad"></span>
254
+ <span class="tr-demo-cbar-label">High</span>
255
+ <div class="tr-demo-cbar-caption">Activity</div>
256
+ </span>
257
+ </div>
258
+ """,
259
+ unsafe_allow_html=True,
260
+ )
261
+
262
+
263
+ def demo_card_close() -> None:
264
+ st.markdown("</div>", unsafe_allow_html=True)
265
+
266
+
267
+ def tr_footer(
268
+ title: str = "CortexLab",
269
+ tagline: str = "Multimodal fMRI brain encoding · open source",
270
+ ) -> None:
271
+ """Render a polished four-column footer.
272
+
273
+ Mirrors Meta-style research-page footers: a brand block on the
274
+ left, three columns of grouped links, and a thin bottom rule with
275
+ legal / build credits. All color-mode aware via the theme variables.
276
+ """
277
+ columns = [
278
+ (
279
+ "Product",
280
+ [
281
+ ("Live demo", "https://huggingface.co/spaces/SID2000/cortexlab-dashboard"),
282
+ ("Toolkit", "https://github.com/siddhant-rajhans/cortexlab"),
283
+ ("Dashboard", "https://github.com/siddhant-rajhans/cortexlab-dashboard"),
284
+ ("Releases", "https://github.com/siddhant-rajhans/cortexlab/releases"),
285
+ ],
286
+ ),
287
+ (
288
+ "Research",
289
+ [
290
+ ("Paper", "https://github.com/siddhant-rajhans/cortexlab"),
291
+ ("BOLD Moments", "https://www.nature.com/articles/s41467-024-50310-3"),
292
+ ("HCP-MMP 1.0", "https://www.nature.com/articles/nature18933"),
293
+ ("CLIP", "https://arxiv.org/abs/2103.00020"),
294
+ ],
295
+ ),
296
+ (
297
+ "Resources",
298
+ [
299
+ ("HuggingFace", "https://huggingface.co/SID2000/cortexlab"),
300
+ ("API docs", "https://github.com/siddhant-rajhans/cortexlab#readme"),
301
+ ("Changelog", "https://github.com/siddhant-rajhans/cortexlab/commits/master"),
302
+ ("Issues", "https://github.com/siddhant-rajhans/cortexlab/issues"),
303
+ ],
304
+ ),
305
+ ]
306
+ cols_html = ""
307
+ for label, links in columns:
308
+ items = "".join(
309
+ f'<li><a class="tr-foot-link" href="{href}" target="_blank">{name}</a></li>'
310
+ for name, href in links
311
+ )
312
+ cols_html += f"""
313
+ <div class="tr-foot-col">
314
+ <div class="tr-foot-col-title">{label}</div>
315
+ <ul class="tr-foot-list">{items}</ul>
316
+ </div>
317
+ """
318
+
319
+ st.markdown(
320
+ f"""
321
+ <div class="tr-foot">
322
+ <div class="tr-foot-grid">
323
+ <div class="tr-foot-brand">
324
+ <div class="tr-foot-brand-logo">
325
+ <span class="tr-foot-dot tr-foot-dot-a"></span>
326
+ <span class="tr-foot-dot tr-foot-dot-b"></span>
327
+ <span class="tr-foot-dot tr-foot-dot-c"></span>
328
+ </div>
329
+ <div class="tr-foot-brand-title">{title}</div>
330
+ <div class="tr-foot-brand-tagline">{tagline}</div>
331
+ <div class="tr-foot-built">
332
+ Built on <a href="https://github.com/facebookresearch/tribev2" target="_blank">Meta's TRIBE v2</a>
333
+ </div>
334
+ </div>
335
+ {cols_html}
336
+ </div>
337
+ <div class="tr-foot-rule"></div>
338
+ <div class="tr-foot-legal">
339
+ <span>© CortexLab Contributors · CC BY-NC 4.0</span>
340
+ <span class="tr-foot-legal-right">
341
+ <a href="https://github.com/siddhant-rajhans/cortexlab/blob/master/LICENSE" target="_blank">License</a>
342
+ <span>·</span>
343
+ <a href="https://github.com/siddhant-rajhans/cortexlab/blob/master/CODE_OF_CONDUCT.md" target="_blank">Conduct</a>
344
+ <span>·</span>
345
+ <a href="https://github.com/siddhant-rajhans/cortexlab" target="_blank">Source</a>
346
+ </span>
347
+ </div>
348
+ </div>
349
+ """,
350
+ unsafe_allow_html=True,
351
+ )
352
+
353
+
354
+ __all__ = [
355
+ "top_header",
356
+ "architecture_steps",
357
+ "pipeline_diagram",
358
+ "segmented",
359
+ "demo_card_open",
360
+ "demo_card_close",
361
+ "tr_footer",
362
+ ]