chrissoria commited on
Commit
3e036e1
·
1 Parent(s): fe6b364

Adopt shared dark-mode theme; promote Llama 3.3 70B as default free model

Browse files

Visual port to bring the Space in line with the desktop app and
catllm.com:
- New theme.py mirrors app/components/css.py from the desktop bundle:
IBM Plex Sans / DM Sans / DM Mono on a dark surface with teal and
orange accents.
- Wordmark now shows the cropped cat-shield + IBM Plex Sans "CatLLM"
with a mono subtitle, replacing the EB Garamond block.
- The 200-line inline CSS in app.py collapses to a single
inject_css() call.
- Reorder FREE_MODELS_MAP so Llama 3.3 70B (HF-routed via Groq, free
for the Space at low volume) is the default selection, with GPT-4o
Mini and friends still available.
- Add a .gitignore so .env and __pycache__/ never reach the Space.

Files changed (4) hide show
  1. .gitignore +10 -0
  2. app.py +14 -218
  3. logo_mark.png +0 -0
  4. theme.py +683 -0
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Local secrets — never push to the Space
2
+ .env
3
+
4
+ # Python bytecode
5
+ __pycache__/
6
+ *.pyc
7
+
8
+ # Local test artifacts
9
+ *.log
10
+ .DS_Store
app.py CHANGED
@@ -12,6 +12,9 @@ import sys
12
  from datetime import datetime
13
  import matplotlib.pyplot as plt
14
 
 
 
 
15
  # Import catllm
16
  try:
17
  import catllm
@@ -92,15 +95,18 @@ def extract_pdf_pages(pdf_paths, pdf_name_map, mode="image"):
92
 
93
  return pages
94
 
95
- # Free models - display name -> actual API model name
 
 
 
96
  FREE_MODELS_MAP = {
 
 
97
  "GPT-4o Mini": "gpt-4o-mini",
98
  "Gemini 2.5 Flash": "gemini-2.5-flash",
99
  "Claude 3 Haiku": "claude-3-haiku-20240307",
100
- "Llama 3.3 70B": "meta-llama/Llama-3.3-70B-Instruct:groq",
101
  "Qwen 2.5": "Qwen/Qwen2.5-72B-Instruct",
102
  "DeepSeek R1": "deepseek-ai/DeepSeek-R1:novita",
103
- "Mistral Medium": "mistral-medium-2505",
104
  "Grok 4 Fast": "grok-4-fast-non-reasoning",
105
  }
106
  FREE_MODEL_DISPLAY_NAMES = list(FREE_MODELS_MAP.keys())
@@ -1096,209 +1102,9 @@ st.set_page_config(
1096
  layout="wide"
1097
  )
1098
 
1099
- # Custom CSS for enhanced styling
1100
- st.markdown("""
1101
- <style>
1102
- /* Import Garamond font and apply globally */
1103
- @import url('https://fonts.googleapis.com/css2?family=EB+Garamond:wght@400;500;600;700&display=swap');
1104
-
1105
- *:not([class*="icon"]):not([data-testid="stIconMaterial"]):not(svg):not(path) {
1106
- font-family: 'EB Garamond', Garamond, Georgia, serif !important;
1107
- font-size: 17px !important;
1108
- }
1109
-
1110
- /* Preserve Streamlit icon fonts */
1111
- [data-testid="stIconMaterial"], .material-icons, .material-symbols-rounded {
1112
- font-family: 'Material Symbols Rounded', 'Material Icons' !important;
1113
- font-size: 24px !important;
1114
- }
1115
-
1116
- /* Main container styling */
1117
- .main .block-container {
1118
- padding-top: 2rem;
1119
- padding-bottom: 2rem;
1120
- }
1121
-
1122
- /* Headers with gradient accent */
1123
- h1 {
1124
- background: linear-gradient(90deg, #E8A33C 0%, #D4872C 100%);
1125
- -webkit-background-clip: text;
1126
- -webkit-text-fill-color: transparent;
1127
- background-clip: text;
1128
- font-weight: 700;
1129
- }
1130
-
1131
- /* Card-like sections */
1132
- .stExpander {
1133
- border: 1px solid #E8D5B5;
1134
- border-radius: 12px;
1135
- box-shadow: 0 2px 8px rgba(232, 163, 60, 0.08);
1136
- }
1137
-
1138
- /* File uploader styling */
1139
- .stFileUploader {
1140
- border-radius: 12px;
1141
- }
1142
-
1143
- .stFileUploader > div > div {
1144
- border: 2px dashed #E8A33C;
1145
- border-radius: 12px;
1146
- background: linear-gradient(135deg, #FEFCF9 0%, #F5EFE6 100%);
1147
- }
1148
-
1149
- /* Button styling */
1150
- .stButton > button {
1151
- border-radius: 8px;
1152
- font-weight: 600;
1153
- transition: all 0.2s ease;
1154
- border: 2px solid #E8A33C;
1155
- background: #FEFCF9;
1156
- color: #D4872C;
1157
- }
1158
-
1159
- /* Tall button for example dataset (matches file uploader height) */
1160
- .tall-button .stButton > button {
1161
- min-height: 107px;
1162
- border-radius: 12px;
1163
- }
1164
-
1165
- .stButton > button:hover {
1166
- transform: translateY(-1px);
1167
- box-shadow: 0 4px 12px rgba(232, 163, 60, 0.3);
1168
- background: #F5EFE6;
1169
- }
1170
-
1171
- /* Primary button */
1172
- .stButton > button[kind="primary"] {
1173
- background: linear-gradient(135deg, #E8A33C 0%, #D4872C 100%);
1174
- border: none;
1175
- color: white;
1176
- }
1177
-
1178
- /* Success/info messages */
1179
- .stSuccess {
1180
- background-color: #E8F5E9;
1181
- border-left: 4px solid #4CAF50;
1182
- border-radius: 0 8px 8px 0;
1183
- }
1184
-
1185
- .stInfo {
1186
- background-color: #FFF8E8;
1187
- border-left: 4px solid #E8A33C;
1188
- border-radius: 0 8px 8px 0;
1189
- }
1190
-
1191
- /* Radio buttons */
1192
- .stRadio > div {
1193
- gap: 0.5rem;
1194
- display: flex;
1195
- width: 100%;
1196
- }
1197
-
1198
- .stRadio > div > label {
1199
- background: #F5EFE6;
1200
- padding: 0.5rem 1rem;
1201
- border-radius: 20px;
1202
- border: 1px solid transparent;
1203
- transition: all 0.2s ease;
1204
- flex: 1;
1205
- text-align: center;
1206
- justify-content: center;
1207
- }
1208
-
1209
- .stRadio > div > label:hover {
1210
- border-color: #E8A33C;
1211
- }
1212
-
1213
- /* Text inputs */
1214
- .stTextInput > div > div > input {
1215
- border-radius: 8px;
1216
- border: 1px solid #E8D5B5;
1217
- }
1218
-
1219
- .stTextInput > div > div > input:focus {
1220
- border-color: #E8A33C;
1221
- box-shadow: 0 0 0 2px rgba(232, 163, 60, 0.2);
1222
- }
1223
-
1224
- /* Select boxes */
1225
- .stSelectbox > div > div {
1226
- border-radius: 8px;
1227
- }
1228
-
1229
- /* Dataframe styling */
1230
- .stDataFrame {
1231
- border-radius: 12px;
1232
- overflow: hidden;
1233
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
1234
- }
1235
-
1236
- /* Progress bar */
1237
- .stProgress > div > div {
1238
- background: linear-gradient(90deg, #E8A33C 0%, #D4872C 100%);
1239
- border-radius: 10px;
1240
- }
1241
-
1242
- /* Slider */
1243
- .stSlider > div > div > div {
1244
- background: #E8A33C;
1245
- }
1246
-
1247
- /* Divider */
1248
- hr {
1249
- border: none;
1250
- height: 1px;
1251
- background: linear-gradient(90deg, transparent, #E8D5B5, transparent);
1252
- margin: 1.5rem 0;
1253
- }
1254
-
1255
- /* Code blocks */
1256
- .stCodeBlock {
1257
- border-radius: 12px;
1258
- border: 1px solid #E8D5B5;
1259
- }
1260
-
1261
- /* Metric cards */
1262
- .stMetric {
1263
- background: linear-gradient(135deg, #FEFCF9 0%, #F5EFE6 100%);
1264
- padding: 1rem;
1265
- border-radius: 12px;
1266
- border: 1px solid #E8D5B5;
1267
- }
1268
-
1269
- /* Download buttons */
1270
- .stDownloadButton > button {
1271
- background: #F5EFE6;
1272
- border: 1px solid #E8A33C;
1273
- color: #D4872C;
1274
- }
1275
-
1276
- .stDownloadButton > button:hover {
1277
- background: #E8A33C;
1278
- color: white;
1279
- }
1280
-
1281
- /* Multiselect */
1282
- .stMultiSelect > div > div {
1283
- border-radius: 8px;
1284
- }
1285
-
1286
- /* Status indicator */
1287
- .stStatus {
1288
- border-radius: 12px;
1289
- }
1290
-
1291
- /* Column gaps */
1292
- [data-testid="column"] {
1293
- padding: 0 0.5rem;
1294
- }
1295
-
1296
- /* Logo and title alignment */
1297
- [data-testid="column"]:first-child img {
1298
- border-radius: 8px;
1299
- }
1300
- </style>
1301
- """, unsafe_allow_html=True)
1302
 
1303
  # Initialize session state
1304
  if 'categories' not in st.session_state:
@@ -1322,18 +1128,8 @@ if 'image_data' not in st.session_state:
1322
  if 'extraction_params' not in st.session_state:
1323
  st.session_state.extraction_params = None # Stores params when categories are auto-extracted
1324
 
1325
- # Logo and title - use HTML for better alignment
1326
- st.markdown("""
1327
- <div style="display: flex; align-items: center; gap: 20px; margin-bottom: 10px;">
1328
- <img src="https://huggingface.co/spaces/CatLLM/survey-classifier/resolve/main/logo.png" width="100" style="border-radius: 8px;">
1329
- <div>
1330
- <div style="font-size: 2.2rem; font-weight: 700; color: #333; font-family: 'EB Garamond', Garamond, Georgia, serif; line-height: 1.1;">CatLLM</div>
1331
- <div style="font-size: 1.1rem; font-weight: 500; color: #E8A33C; font-family: 'EB Garamond', Garamond, Georgia, serif; margin-bottom: 4px;">NLP for Survey Research</div>
1332
- <div style="font-size: 1rem; font-weight: 400; color: #666; font-family: 'EB Garamond', Garamond, Georgia, serif;">Research-grade categorization of survey responses, PDFs, and images using AI models.</div>
1333
- <div style="font-size: 0.85rem; font-weight: 400; color: #888; font-family: 'EB Garamond', Garamond, Georgia, serif; margin-top: 4px;">Developed at UC Berkeley</div>
1334
- </div>
1335
- </div>
1336
- """, unsafe_allow_html=True)
1337
 
1338
  # About section
1339
  with st.expander("About This App"):
 
12
  from datetime import datetime
13
  import matplotlib.pyplot as plt
14
 
15
+ # Brand theme — shared with the desktop app and catllm.com.
16
+ from theme import inject_css, render_wordmark
17
+
18
  # Import catllm
19
  try:
20
  import catllm
 
95
 
96
  return pages
97
 
98
+ # Free models - display name -> actual API model name.
99
+ # Ordered so the cheapest-for-the-Space option lands first (Streamlit
100
+ # selectboxes default to index 0). Llama 3.3 70B is routed through HF's
101
+ # Inference API via Groq and is effectively free at low volume.
102
  FREE_MODELS_MAP = {
103
+ "Llama 3.3 70B": "meta-llama/Llama-3.3-70B-Instruct:groq",
104
+ "Mistral Medium": "mistral-medium-2505",
105
  "GPT-4o Mini": "gpt-4o-mini",
106
  "Gemini 2.5 Flash": "gemini-2.5-flash",
107
  "Claude 3 Haiku": "claude-3-haiku-20240307",
 
108
  "Qwen 2.5": "Qwen/Qwen2.5-72B-Instruct",
109
  "DeepSeek R1": "deepseek-ai/DeepSeek-R1:novita",
 
110
  "Grok 4 Fast": "grok-4-fast-non-reasoning",
111
  }
112
  FREE_MODEL_DISPLAY_NAMES = list(FREE_MODELS_MAP.keys())
 
1102
  layout="wide"
1103
  )
1104
 
1105
+ # Brand theme IBM Plex Sans / DM Sans / DM Mono, dark mode.
1106
+ # Same theme as the desktop app and catllm.com (theme.py).
1107
+ inject_css()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1108
 
1109
  # Initialize session state
1110
  if 'categories' not in st.session_state:
 
1128
  if 'extraction_params' not in st.session_state:
1129
  st.session_state.extraction_params = None # Stores params when categories are auto-extracted
1130
 
1131
+ # Wordmark: cat-shield mark + IBM Plex Sans "CatLLM" + mono subtitle.
1132
+ render_wordmark("NLP for survey research · UC Berkeley")
 
 
 
 
 
 
 
 
 
 
1133
 
1134
  # About section
1135
  with st.expander("About This App"):
logo_mark.png ADDED
theme.py ADDED
@@ -0,0 +1,683 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CatLLM brand CSS theme for the HuggingFace Space.
3
+
4
+ Mirrors the catllm.com site palette and the desktop app: dark mode with
5
+ teal (primary) and orange (secondary) accents, paired with IBM Plex Sans
6
+ (display), DM Sans (body), and DM Mono (codes & metadata).
7
+
8
+ Kept in sync with app/components/css.py — copy that file over when the
9
+ desktop theme changes.
10
+ """
11
+
12
+ import base64
13
+ import os
14
+
15
+ import streamlit as st
16
+
17
+
18
+ def inject_css():
19
+ """Inject the CatLLM CSS theme into the page."""
20
+ st.markdown(CSS, unsafe_allow_html=True)
21
+
22
+
23
+ def _load_logo_uri(filename: str = "logo_mark.png") -> str:
24
+ """Return a base64 data URI for the cat-shield mark, or empty string."""
25
+ path = os.path.join(os.path.dirname(__file__), filename)
26
+ try:
27
+ with open(path, "rb") as f:
28
+ return "data:image/png;base64," + base64.b64encode(f.read()).decode("ascii")
29
+ except FileNotFoundError:
30
+ return ""
31
+
32
+
33
+ LOGO_MARK_URI = _load_logo_uri()
34
+
35
+
36
+ def render_wordmark(subtitle: str = "Research-grade text classification with LLMs"):
37
+ """Render the CatLLM cat-shield + IBM Plex Sans wordmark + a short subtitle."""
38
+ st.markdown(
39
+ f"""
40
+ <div style="display:flex; align-items:center; gap:0.75rem; margin: 0 0 0.5rem 0; padding-bottom:0.75rem; border-bottom:1px solid rgba(232,237,245,0.10);">
41
+ <img src="{LOGO_MARK_URI}" alt="" style="height:54px; width:auto; display:block; flex-shrink:0; transform:translateY(-4px);">
42
+ <div style="display:flex; flex-direction:column; gap:0.15rem;">
43
+ <span style="font-family:'IBM Plex Sans',sans-serif; font-size:2.1rem; line-height:1; font-weight:600; letter-spacing:-0.025em; color:#E8EDF5;">CatLLM</span>
44
+ <span style="font-family:'DM Mono',monospace; font-size:0.72rem; letter-spacing:0.14em; text-transform:uppercase; color:#8A95A8;">{subtitle}</span>
45
+ </div>
46
+ </div>
47
+ """,
48
+ unsafe_allow_html=True,
49
+ )
50
+
51
+
52
+ CSS = """
53
+ <style>
54
+ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=DM+Sans:ital,opsz,wght@0,9..40,300..700;1,9..40,300..700&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400&display=swap');
55
+
56
+ :root {
57
+ --bg: #1A1A1A;
58
+ --bg-2: #1F1F1F;
59
+ --bg-3: #262626;
60
+ --bg-4: #2E2E2E;
61
+ --teal: #00DCC3;
62
+ --teal-dim: #00A89A;
63
+ --teal-glow: rgba(0, 220, 195, 0.12);
64
+ --teal-glow2: rgba(0, 220, 195, 0.04);
65
+ --orange: #FF8C3C;
66
+ --orange-dim: #D87024;
67
+ --orange-glow: rgba(255, 140, 60, 0.12);
68
+ --text: #E8EDF5;
69
+ --text-2: #8A95A8;
70
+ --text-3: #4E5A6D;
71
+ --hairline: rgba(232, 237, 245, 0.10);
72
+ --hairline-strong: rgba(232, 237, 245, 0.22);
73
+ --shadow: rgba(0, 0, 0, 0.45);
74
+ }
75
+
76
+ /* ===================================================================
77
+ Global typography — DM Sans body, IBM Plex Sans display, DM Mono code.
78
+ =================================================================== */
79
+ *:not([class*="icon"]):not([data-testid="stIconMaterial"]):not(svg):not(path):not(.material-icons):not(.material-symbols-rounded) {
80
+ font-family: 'DM Sans', system-ui, -apple-system, sans-serif !important;
81
+ font-size: 15px;
82
+ color: var(--text);
83
+ -webkit-font-smoothing: antialiased;
84
+ text-rendering: optimizeLegibility;
85
+ }
86
+
87
+ [data-testid="stIconMaterial"], .material-icons, .material-symbols-rounded {
88
+ font-family: 'Material Symbols Rounded', 'Material Icons' !important;
89
+ font-size: 22px !important;
90
+ }
91
+
92
+ h1, h2, h3, h4, .clm-display {
93
+ font-family: 'IBM Plex Sans', system-ui, sans-serif !important;
94
+ font-weight: 600 !important;
95
+ letter-spacing: -0.02em;
96
+ color: var(--text) !important;
97
+ background: none !important;
98
+ -webkit-text-fill-color: var(--text) !important;
99
+ }
100
+
101
+ h1 { font-size: 2.5rem !important; line-height: 1.05 !important; }
102
+ h2 { font-size: 1.85rem !important; line-height: 1.1; }
103
+ h3 { font-size: 1.25rem !important; line-height: 1.2; font-weight: 500 !important; }
104
+ h4 { font-size: 1.05rem !important; line-height: 1.25; font-weight: 500 !important; }
105
+
106
+ em, i, .italic {
107
+ font-style: italic;
108
+ color: var(--text-2);
109
+ }
110
+
111
+ code, kbd, pre, .mono, .clm-mono,
112
+ .stCodeBlock, .stCode, [class*="language-"] {
113
+ font-family: 'DM Mono', ui-monospace, 'JetBrains Mono', monospace !important;
114
+ font-size: 0.85em;
115
+ }
116
+
117
+ /* ===================================================================
118
+ Page background — deep ink with subtle teal/orange auras.
119
+ =================================================================== */
120
+ html, body,
121
+ [data-testid="stAppViewContainer"],
122
+ [data-testid="stMain"],
123
+ .main {
124
+ background-color: var(--bg) !important;
125
+ background-image:
126
+ radial-gradient(ellipse at top left, rgba(0, 220, 195, 0.06) 0%, transparent 55%),
127
+ radial-gradient(ellipse at bottom right, rgba(255, 140, 60, 0.05) 0%, transparent 55%) !important;
128
+ background-attachment: fixed, fixed !important;
129
+ color: var(--text) !important;
130
+ }
131
+
132
+ [data-testid="stHeader"] {
133
+ background: rgba(26, 26, 26, 0.75) !important;
134
+ backdrop-filter: blur(8px);
135
+ border-bottom: 1px solid var(--hairline) !important;
136
+ }
137
+
138
+ .main .block-container {
139
+ padding-top: 2.5rem;
140
+ padding-bottom: 4rem;
141
+ max-width: 1180px;
142
+ }
143
+
144
+ /* ===================================================================
145
+ Buttons — soft surface tile, teal halo on hover.
146
+ =================================================================== */
147
+ .stButton > button,
148
+ .stDownloadButton > button,
149
+ .stFormSubmitButton > button {
150
+ border-radius: 8px !important;
151
+ font-family: 'DM Sans', sans-serif !important;
152
+ font-size: 0.92rem !important;
153
+ font-weight: 500 !important;
154
+ letter-spacing: 0 !important;
155
+ text-transform: none !important;
156
+ border: 1px solid var(--hairline-strong) !important;
157
+ background: var(--bg-3) !important;
158
+ color: var(--text) !important;
159
+ padding: 0.65rem 1.2rem !important;
160
+ transition: background 140ms ease, border-color 140ms ease, color 140ms ease, box-shadow 140ms ease, transform 140ms ease !important;
161
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 1px 2px var(--shadow) !important;
162
+ white-space: normal !important;
163
+ line-height: 1.3 !important;
164
+ }
165
+
166
+ .stButton > button:hover,
167
+ .stDownloadButton > button:hover,
168
+ .stFormSubmitButton > button:hover {
169
+ border-color: var(--teal-dim) !important;
170
+ background: var(--bg-4) !important;
171
+ color: var(--teal) !important;
172
+ box-shadow: 0 0 0 1px var(--teal-glow), 0 8px 24px var(--teal-glow2), 0 1px 0 rgba(255,255,255,0.04) inset !important;
173
+ }
174
+
175
+ .stButton > button:active,
176
+ .stDownloadButton > button:active,
177
+ .stFormSubmitButton > button:active {
178
+ transform: translateY(1px) !important;
179
+ }
180
+
181
+ .stButton > button[kind="primary"],
182
+ .stFormSubmitButton > button[kind="primary"] {
183
+ background: var(--teal) !important;
184
+ border-color: var(--teal) !important;
185
+ color: var(--bg) !important;
186
+ font-weight: 600 !important;
187
+ box-shadow: 0 0 0 1px var(--teal-glow), 0 6px 18px var(--teal-glow) !important;
188
+ }
189
+
190
+ .stButton > button[kind="primary"]:hover,
191
+ .stFormSubmitButton > button[kind="primary"]:hover {
192
+ background: var(--teal-dim) !important;
193
+ border-color: var(--teal-dim) !important;
194
+ color: var(--bg) !important;
195
+ }
196
+
197
+ .tall-button .stButton > button {
198
+ min-height: 107px;
199
+ border-radius: 8px !important;
200
+ font-size: 0.9rem !important;
201
+ }
202
+
203
+ /* ===================================================================
204
+ File uploader.
205
+ =================================================================== */
206
+ .stFileUploader {
207
+ margin-top: 1.25rem !important;
208
+ }
209
+
210
+ .stFileUploader > div > div,
211
+ [data-testid="stFileUploaderDropzone"] {
212
+ border: 1px dashed var(--hairline-strong) !important;
213
+ border-radius: 10px !important;
214
+ background: var(--bg-2) !important;
215
+ transition: border-color 160ms ease, background 160ms ease !important;
216
+ }
217
+
218
+ [data-testid="stFileUploaderDropzone"]:hover {
219
+ border-color: var(--teal-dim) !important;
220
+ background: var(--bg-3) !important;
221
+ }
222
+
223
+ [data-testid="stFileUploaderDropzone"] small,
224
+ [data-testid="stFileUploaderDropzone"] span {
225
+ color: var(--text-2) !important;
226
+ }
227
+
228
+ /* "Browse files" button inside the dropzone — overrides Streamlit's
229
+ default light secondary-button styling. */
230
+ [data-testid="stFileUploaderDropzone"] button,
231
+ .stFileUploader button {
232
+ background: var(--bg-3) !important;
233
+ color: var(--text) !important;
234
+ border: 1px solid var(--hairline-strong) !important;
235
+ border-radius: 6px !important;
236
+ font-family: 'DM Sans', sans-serif !important;
237
+ font-size: 0.85rem !important;
238
+ font-weight: 500 !important;
239
+ padding: 0.45rem 1rem !important;
240
+ box-shadow: none !important;
241
+ transition: background 140ms ease, border-color 140ms ease, color 140ms ease !important;
242
+ }
243
+
244
+ [data-testid="stFileUploaderDropzone"] button:hover,
245
+ .stFileUploader button:hover {
246
+ background: var(--bg-4) !important;
247
+ border-color: var(--teal-dim) !important;
248
+ color: var(--teal) !important;
249
+ }
250
+
251
+ /* Dropzone cloud icon */
252
+ [data-testid="stFileUploaderDropzone"] svg {
253
+ fill: var(--text-2) !important;
254
+ color: var(--text-2) !important;
255
+ }
256
+
257
+ /* ===================================================================
258
+ Inputs.
259
+ =================================================================== */
260
+ .stTextInput > div > div > input,
261
+ .stTextArea textarea,
262
+ .stNumberInput input,
263
+ .stChatInput textarea {
264
+ border-radius: 8px !important;
265
+ border: 1px solid var(--hairline-strong) !important;
266
+ background: var(--bg-2) !important;
267
+ color: var(--text) !important;
268
+ font-family: 'DM Sans', sans-serif !important;
269
+ padding: 0.55rem 0.85rem !important;
270
+ transition: border-color 140ms ease, box-shadow 140ms ease !important;
271
+ }
272
+
273
+ .stTextInput > div > div > input:focus,
274
+ .stTextArea textarea:focus,
275
+ .stNumberInput input:focus {
276
+ border-color: var(--teal) !important;
277
+ box-shadow: 0 0 0 3px var(--teal-glow) !important;
278
+ outline: none !important;
279
+ }
280
+
281
+ .stSelectbox > div > div,
282
+ .stMultiSelect > div > div {
283
+ border-radius: 8px !important;
284
+ border: 1px solid var(--hairline-strong) !important;
285
+ background: var(--bg-2) !important;
286
+ }
287
+
288
+ .stSelectbox > div > div:hover,
289
+ .stMultiSelect > div > div:hover {
290
+ border-color: var(--text-3) !important;
291
+ }
292
+
293
+ [data-baseweb="tag"] {
294
+ border-radius: 6px !important;
295
+ background: var(--teal-glow) !important;
296
+ color: var(--teal) !important;
297
+ border: 1px solid var(--teal-dim) !important;
298
+ font-family: 'DM Mono', monospace !important;
299
+ font-size: 0.78rem !important;
300
+ }
301
+
302
+ /* Select dropdown menu — BaseWeb popovers used by Streamlit selectbox /
303
+ multiselect / date-picker. Default Streamlit theme leaves these light. */
304
+ [data-baseweb="popover"],
305
+ [data-baseweb="popover"] > div,
306
+ [data-baseweb="menu"],
307
+ [role="listbox"],
308
+ [data-baseweb="select-dropdown"],
309
+ ul[role="listbox"] {
310
+ background: var(--bg-3) !important;
311
+ background-color: var(--bg-3) !important;
312
+ border: 1px solid var(--hairline-strong) !important;
313
+ border-radius: 8px !important;
314
+ color: var(--text) !important;
315
+ box-shadow: 0 8px 24px var(--shadow) !important;
316
+ }
317
+
318
+ [data-baseweb="menu"] li,
319
+ [role="option"],
320
+ [role="listbox"] li {
321
+ background: transparent !important;
322
+ color: var(--text) !important;
323
+ font-family: 'DM Sans', sans-serif !important;
324
+ padding: 0.55rem 0.85rem !important;
325
+ }
326
+
327
+ [data-baseweb="menu"] li:hover,
328
+ [role="option"]:hover,
329
+ [role="option"][aria-selected="true"],
330
+ [role="listbox"] li:hover {
331
+ background: var(--bg-4) !important;
332
+ color: var(--teal) !important;
333
+ }
334
+
335
+ /* The closed selectbox itself — make sure background stays dark. */
336
+ [data-baseweb="select"] > div,
337
+ [data-baseweb="select"] [data-baseweb="select-control"] {
338
+ background: var(--bg-2) !important;
339
+ color: var(--text) !important;
340
+ }
341
+
342
+ [data-baseweb="select"] input,
343
+ [data-baseweb="select"] [data-baseweb="select-content"] {
344
+ color: var(--text) !important;
345
+ background: transparent !important;
346
+ }
347
+
348
+ /* ===================================================================
349
+ Radio — pill row.
350
+ =================================================================== */
351
+ .stRadio > div {
352
+ gap: 0.5rem;
353
+ display: flex;
354
+ flex-wrap: wrap;
355
+ }
356
+
357
+ .stRadio > div > label {
358
+ background: var(--bg-2);
359
+ padding: 0.55rem 0.95rem;
360
+ border-radius: 8px;
361
+ border: 1px solid var(--hairline-strong);
362
+ transition: all 150ms ease;
363
+ flex: 1;
364
+ min-width: 0;
365
+ text-align: center;
366
+ justify-content: center;
367
+ font-family: 'DM Sans', sans-serif !important;
368
+ font-size: 0.92rem !important;
369
+ cursor: pointer;
370
+ color: var(--text-2) !important;
371
+ }
372
+
373
+ .stRadio > div > label:hover {
374
+ border-color: var(--teal-dim);
375
+ color: var(--text) !important;
376
+ }
377
+
378
+ .stRadio > div > label:has(input:checked) {
379
+ background: var(--teal-glow) !important;
380
+ border-color: var(--teal) !important;
381
+ color: var(--teal) !important;
382
+ }
383
+ .stRadio > div > label:has(input:checked) * {
384
+ color: var(--teal) !important;
385
+ }
386
+
387
+ /* ===================================================================
388
+ Expanders.
389
+ =================================================================== */
390
+ .stExpander,
391
+ [data-testid="stExpander"] {
392
+ border: 1px solid var(--hairline-strong) !important;
393
+ border-radius: 10px !important;
394
+ background: var(--bg-2) !important;
395
+ box-shadow: none !important;
396
+ }
397
+
398
+ .stExpander summary,
399
+ [data-testid="stExpander"] summary {
400
+ font-family: 'IBM Plex Sans', sans-serif !important;
401
+ font-weight: 500 !important;
402
+ font-size: 0.95rem !important;
403
+ color: var(--text) !important;
404
+ padding: 0.75rem 1rem !important;
405
+ }
406
+
407
+ .stExpander summary:hover {
408
+ color: var(--teal) !important;
409
+ }
410
+
411
+ /* ===================================================================
412
+ Messages.
413
+ =================================================================== */
414
+ .stSuccess {
415
+ background: rgba(0, 220, 195, 0.08) !important;
416
+ border: 1px solid var(--teal-dim) !important;
417
+ border-radius: 8px !important;
418
+ color: var(--text) !important;
419
+ }
420
+ .stInfo {
421
+ background: var(--bg-2) !important;
422
+ border: 1px solid var(--hairline-strong) !important;
423
+ border-left: 3px solid var(--teal) !important;
424
+ border-radius: 8px !important;
425
+ color: var(--text-2) !important;
426
+ }
427
+ .stWarning {
428
+ background: rgba(255, 140, 60, 0.06) !important;
429
+ border: 1px solid var(--orange-dim) !important;
430
+ border-radius: 8px !important;
431
+ color: var(--text) !important;
432
+ }
433
+ .stError {
434
+ background: rgba(255, 95, 87, 0.06) !important;
435
+ border: 1px solid #ff5f57 !important;
436
+ border-radius: 8px !important;
437
+ color: var(--text) !important;
438
+ }
439
+
440
+ /* ===================================================================
441
+ Metric cards.
442
+ =================================================================== */
443
+ .stMetric, [data-testid="stMetric"] {
444
+ background: var(--bg-2) !important;
445
+ padding: 1rem 1.1rem !important;
446
+ border-radius: 10px !important;
447
+ border: 1px solid var(--hairline-strong) !important;
448
+ box-shadow: none !important;
449
+ }
450
+
451
+ [data-testid="stMetricLabel"] {
452
+ font-family: 'DM Mono', monospace !important;
453
+ font-size: 0.7rem !important;
454
+ text-transform: uppercase;
455
+ letter-spacing: 0.12em !important;
456
+ color: var(--text-3) !important;
457
+ }
458
+
459
+ [data-testid="stMetricValue"] {
460
+ font-family: 'IBM Plex Sans', sans-serif !important;
461
+ font-size: 2rem !important;
462
+ line-height: 1.05 !important;
463
+ color: var(--text) !important;
464
+ font-weight: 600 !important;
465
+ }
466
+
467
+ [data-testid="stMetricDelta"] {
468
+ color: var(--teal) !important;
469
+ }
470
+
471
+ /* ===================================================================
472
+ Dataframe.
473
+ =================================================================== */
474
+ .stDataFrame, [data-testid="stDataFrame"] {
475
+ border-radius: 10px !important;
476
+ overflow: hidden;
477
+ border: 1px solid var(--hairline-strong) !important;
478
+ }
479
+
480
+ .stDataFrame thead th {
481
+ background: var(--bg-3) !important;
482
+ color: var(--text-2) !important;
483
+ font-family: 'DM Mono', monospace !important;
484
+ text-transform: uppercase;
485
+ font-size: 0.72rem !important;
486
+ letter-spacing: 0.08em !important;
487
+ border-bottom: 1px solid var(--hairline-strong) !important;
488
+ }
489
+
490
+ .stDataFrame tbody td {
491
+ background: var(--bg-2) !important;
492
+ color: var(--text) !important;
493
+ border-bottom: 1px solid var(--hairline) !important;
494
+ }
495
+
496
+ /* ===================================================================
497
+ Progress & slider.
498
+ =================================================================== */
499
+ .stProgress > div > div {
500
+ background: var(--teal) !important;
501
+ border-radius: 4px !important;
502
+ }
503
+ .stProgress > div {
504
+ background: var(--bg-3) !important;
505
+ border-radius: 4px !important;
506
+ }
507
+
508
+ .stSlider [role="slider"] {
509
+ background: var(--teal) !important;
510
+ border: 2px solid var(--bg) !important;
511
+ box-shadow: 0 0 0 1px var(--teal-dim);
512
+ border-radius: 50% !important;
513
+ }
514
+ .stSlider > div > div > div > div {
515
+ background: var(--teal) !important;
516
+ }
517
+
518
+ /* ===================================================================
519
+ Dividers.
520
+ =================================================================== */
521
+ hr {
522
+ border: none !important;
523
+ height: 1px !important;
524
+ background: var(--hairline-strong) !important;
525
+ margin: 1.75rem 0 !important;
526
+ }
527
+
528
+ /* ===================================================================
529
+ Code blocks — teal-tinted dark.
530
+ =================================================================== */
531
+ .stCodeBlock, pre {
532
+ border-radius: 10px !important;
533
+ border: 1px solid var(--hairline-strong) !important;
534
+ background: #0F0F0F !important;
535
+ }
536
+
537
+ .stCodeBlock code, pre code {
538
+ color: var(--text) !important;
539
+ font-family: 'DM Mono', monospace !important;
540
+ font-size: 0.85rem !important;
541
+ }
542
+
543
+ /* ===================================================================
544
+ Tabs.
545
+ =================================================================== */
546
+ .stTabs [data-baseweb="tab-list"] {
547
+ border-bottom: 1px solid var(--hairline-strong) !important;
548
+ gap: 0 !important;
549
+ background: transparent !important;
550
+ }
551
+
552
+ .stTabs [data-baseweb="tab"] {
553
+ font-family: 'DM Sans', sans-serif !important;
554
+ font-size: 0.92rem !important;
555
+ font-weight: 500 !important;
556
+ padding: 0.75rem 1.1rem !important;
557
+ border-radius: 0 !important;
558
+ color: var(--text-2) !important;
559
+ background: transparent !important;
560
+ }
561
+
562
+ .stTabs [aria-selected="true"] {
563
+ color: var(--teal) !important;
564
+ background: transparent !important;
565
+ border-bottom: 2px solid var(--teal) !important;
566
+ }
567
+
568
+ /* ===================================================================
569
+ Sidebar.
570
+ =================================================================== */
571
+ [data-testid="stSidebar"] {
572
+ background: var(--bg-2) !important;
573
+ border-right: 1px solid var(--hairline-strong) !important;
574
+ }
575
+
576
+ /* Collapse Streamlit's generous default top padding in the sidebar so the
577
+ wordmark sits close to the top edge. */
578
+ [data-testid="stSidebar"] > div,
579
+ [data-testid="stSidebar"] section,
580
+ [data-testid="stSidebar"] [data-testid="stSidebarContent"],
581
+ [data-testid="stSidebar"] [data-testid="stSidebarUserContent"],
582
+ [data-testid="stSidebar"] [data-testid="stSidebarNav"] + div {
583
+ padding-top: 0.25rem !important;
584
+ }
585
+
586
+ [data-testid="stSidebar"] [data-testid="stVerticalBlock"]:first-child {
587
+ margin-top: 0 !important;
588
+ padding-top: 0 !important;
589
+ gap: 0.6rem !important;
590
+ }
591
+
592
+ [data-testid="stSidebar"] [data-testid="stVerticalBlock"]:first-child > div:first-child {
593
+ margin-top: 0 !important;
594
+ padding-top: 0 !important;
595
+ }
596
+
597
+ /* Also collapse the global block-container top padding on the main column
598
+ so the layout reads tighter overall. */
599
+ [data-testid="stSidebar"] .stMarkdown:first-child,
600
+ [data-testid="stSidebar"] [data-testid="stMarkdownContainer"]:first-child {
601
+ margin-top: 0 !important;
602
+ }
603
+
604
+ [data-testid="stSidebar"] h1,
605
+ [data-testid="stSidebar"] h2,
606
+ [data-testid="stSidebar"] h3,
607
+ [data-testid="stSidebar"] [data-testid="stMarkdown"] h2 {
608
+ font-family: 'IBM Plex Sans', sans-serif !important;
609
+ font-weight: 600 !important;
610
+ letter-spacing: -0.02em;
611
+ font-size: 1.2rem !important;
612
+ color: var(--text) !important;
613
+ margin-bottom: 0.5rem !important;
614
+ }
615
+
616
+ [data-testid="stSidebar"] .stSelectbox label,
617
+ [data-testid="stSidebar"] .stRadio > label,
618
+ [data-testid="stSidebar"] [data-testid="stWidgetLabel"] label {
619
+ font-family: 'DM Mono', monospace !important;
620
+ font-size: 0.72rem !important;
621
+ text-transform: uppercase;
622
+ letter-spacing: 0.12em !important;
623
+ color: var(--text-3) !important;
624
+ font-weight: 400 !important;
625
+ }
626
+
627
+ /* ===================================================================
628
+ Links.
629
+ =================================================================== */
630
+ a, a:visited {
631
+ color: var(--teal) !important;
632
+ text-decoration: underline;
633
+ text-decoration-thickness: 1px;
634
+ text-underline-offset: 3px;
635
+ text-decoration-color: var(--teal-dim);
636
+ transition: color 140ms ease, text-decoration-color 140ms ease;
637
+ }
638
+
639
+ a:hover {
640
+ color: var(--teal) !important;
641
+ text-decoration-color: var(--teal) !important;
642
+ }
643
+
644
+ /* ===================================================================
645
+ Status & toast.
646
+ =================================================================== */
647
+ .stStatus, [data-testid="stStatusWidget"] {
648
+ border-radius: 10px !important;
649
+ border: 1px solid var(--hairline-strong) !important;
650
+ background: var(--bg-2) !important;
651
+ }
652
+
653
+ /* Scrollbar styling */
654
+ ::-webkit-scrollbar {
655
+ width: 10px;
656
+ height: 10px;
657
+ }
658
+ ::-webkit-scrollbar-track {
659
+ background: var(--bg);
660
+ }
661
+ ::-webkit-scrollbar-thumb {
662
+ background: var(--bg-4);
663
+ border-radius: 5px;
664
+ border: 2px solid var(--bg);
665
+ }
666
+ ::-webkit-scrollbar-thumb:hover {
667
+ background: var(--text-3);
668
+ }
669
+
670
+ footer { visibility: hidden; }
671
+
672
+ /* ===================================================================
673
+ Utility classes.
674
+ =================================================================== */
675
+ .clm-kicker {
676
+ font-family: 'DM Mono', monospace !important;
677
+ font-size: 0.72rem !important;
678
+ text-transform: uppercase;
679
+ letter-spacing: 0.16em;
680
+ color: var(--text-3);
681
+ }
682
+ </style>
683
+ """