Spaces:
Sleeping
TRIBE-style landing redesign with black/white theme toggle
Browse filesRebuild 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.
- .streamlit/config.toml +2 -2
- Dockerfile +1 -1
- Home.py +276 -139
- assets/theme.css +716 -2
- assets/theme_black.css +60 -0
- assets/theme_white.css +245 -0
- brain_mesh.py +80 -10
- theme.py +175 -54
- tribe.py +362 -0
|
@@ -1,8 +1,8 @@
|
|
| 1 |
[theme]
|
| 2 |
base = "dark"
|
| 3 |
primaryColor = "#7C3AED"
|
| 4 |
-
backgroundColor = "#
|
| 5 |
-
secondaryBackgroundColor = "#
|
| 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 |
|
|
@@ -13,4 +13,4 @@ COPY . .
|
|
| 13 |
|
| 14 |
EXPOSE 7860
|
| 15 |
|
| 16 |
-
CMD ["streamlit", "run", "
|
|
|
|
| 13 |
|
| 14 |
EXPOSE 7860
|
| 15 |
|
| 16 |
+
CMD ["streamlit", "run", "Home.py", "--server.port=7860", "--server.headless=true", "--server.address=0.0.0.0"]
|
|
@@ -1,159 +1,296 @@
|
|
| 1 |
-
"""CortexLab Dashboard -
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
import streamlit as st
|
| 4 |
import numpy as np
|
|
|
|
| 5 |
|
| 6 |
-
from theme import
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
from utils import make_roi_indices
|
| 9 |
-
from brain_mesh import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
st.set_page_config(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
init_session()
|
| 13 |
inject_theme()
|
| 14 |
|
| 15 |
-
# ---
|
| 16 |
-
|
| 17 |
-
"CortexLab",
|
| 18 |
-
"
|
|
|
|
| 19 |
)
|
| 20 |
|
| 21 |
-
# ---
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
with
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
)
|
| 41 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 42 |
|
| 43 |
-
|
| 44 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
c1, c2, c3, c4, c5 = st.columns(5)
|
| 46 |
-
with c1: glow_card("Tests", "
|
| 47 |
-
with c2: glow_card("
|
| 48 |
with c3: glow_card("ROIs", "29", "HCP MMP1.0", "#3B82F6")
|
| 49 |
-
with c4: glow_card("
|
| 50 |
-
with c5: glow_card("
|
| 51 |
|
| 52 |
-
# ---
|
| 53 |
st.markdown("<div style='height: 1.5rem'></div>", unsafe_allow_html=True)
|
| 54 |
-
section_header("Analysis
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
),
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 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 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 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 |
-
•
|
| 153 |
-
<a href="https://github.com/siddhant-rajhans/cortexlab" style="color: #3B82F6; text-decoration: none;">GitHub</a>
|
| 154 |
-
•
|
| 155 |
-
<a href="https://huggingface.co/SID2000/cortexlab" style="color: #06B6D4; text-decoration: none;">HuggingFace</a>
|
| 156 |
-
•
|
| 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 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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
|
| 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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
@@ -4,13 +4,37 @@ import streamlit as st
|
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
<
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
|
| 95 |
|
| 96 |
def section_header(title, description=""):
|
| 97 |
-
"""Render a styled section header with optional description.
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
"
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
|
| 114 |
def feature_card(icon, title, description, color="#7C3AED"):
|
| 115 |
-
"""Render a feature card for the home page.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
return f"""
|
| 117 |
-
<
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
"
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
"""
|
|
@@ -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 |
+
]
|