zephyrie Claude Opus 4.7 commited on
Commit
2e66cee
Β·
1 Parent(s): 68d0499

Add adaptive clinical dual-theme (light + dark)

Browse files

Replaces the force-dark hack with a real adaptive dual-theme that follows
the visitor's OS / HF Spaces preference.

- Tokenize every color in the CSS block: ~25 existing vars kept, ~20 new
semantic tokens added for previously-hardcoded literals. Accent-tinted
fills now use color-mix() against an accent token, which resolves
pixel-exact in dark (e.g. --green #76b900 == rgb(118,185,0)).
- Define two token blocks mirroring Gradio's own selectors: :root carries
the new light "clinical blue" palette; :root.dark, :root .dark carries
the dark "MAISI Console" palette (values copied verbatim, so dark mode
is unchanged). Gradio toggles the .dark class from the user preference.
- The niivue viewer surface and its empty-state placeholder stay dark in
both themes (radiology/PACS convention); their colors are not tokenized.
- Modality dots (.ws-dot) in the three workspaces now use var(--ct/mr/mrb)
so they pick up the deepened light-mode modality colors.
- Remove _all_dark_theme(); use stock gr.themes.Base(). head meta is now
color-scheme: light dark so native chrome adapts.

Verified with Playwright against gradio 6.14.0: hero + CT workspace render
correctly in both ?__theme=light and ?__theme=dark; viewer stays dark;
no console errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Files changed (4) hide show
  1. app.py +149 -86
  2. ui/workspace_ct.py +1 -1
  3. ui/workspace_mr.py +1 -1
  4. ui/workspace_mr_brain.py +1 -1
app.py CHANGED
@@ -63,13 +63,82 @@ from ui import workspace_ct, workspace_mr, workspace_mr_brain
63
  CSS = """
64
  @import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap');
65
 
 
 
 
 
 
 
 
 
 
 
66
  :root {
67
- /* Navy / blue-black palette β€” distinctly blue, never gray */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  --bg-0: #06102a;
69
  --bg-1: #0a1530;
70
  --bg-2: #0e1838;
71
  --panel: #0e1b3a;
72
  --panel-2: #142348;
 
73
  --line: rgba(140, 180, 240, 0.12);
74
  --line-strong: rgba(140, 180, 240, 0.22);
75
  --line-bright: rgba(140, 180, 240, 0.38);
@@ -88,12 +157,21 @@ CSS = """
88
  --mr: #5fb4ff;
89
  --mrb: #b48aff;
90
 
91
- --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
92
- --font-mono: "Geist Mono", ui-monospace, "JetBrains Mono", "SF Mono", monospace;
93
- --num: "Geist Mono", ui-monospace, monospace;
94
-
95
- --container: 1240px;
96
- --gutter: 32px;
 
 
 
 
 
 
 
 
 
97
  }
98
 
99
  /* ─────────────── base + blueprint grid ─────────────── */
@@ -114,7 +192,7 @@ gradio-app {
114
  --color-background-primary: transparent !important;
115
  }
116
 
117
- html { background: #010206 !important; }
118
  html, body {
119
  color: var(--text) !important;
120
  font-family: var(--font-sans);
@@ -124,19 +202,19 @@ html, body {
124
  body {
125
  background-image:
126
  /* Concentrated blue glow behind the hero only β€” not page-wide */
127
- radial-gradient(ellipse 1100px 600px at 50% 220px, rgba(70, 140, 230, 0.16), transparent 65%),
128
  /* NVIDIA green hint warming the top-left of the page */
129
- radial-gradient(ellipse 700px 380px at 18% -40px, rgba(118, 185, 0, 0.06), transparent 70%),
130
  /* Cool purple sweep at lower-right (very faint) */
131
- radial-gradient(ellipse 900px 600px at 108% 108%, rgba(140, 100, 220, 0.06), transparent 60%),
132
- /* Architectural blueprint grid β€” dark lines for blueprint-paper feel */
133
- linear-gradient(rgba(0, 0, 6, 0.42) 1px, transparent 1px),
134
- linear-gradient(90deg, rgba(0, 0, 6, 0.42) 1px, transparent 1px),
135
- /* Near-black base with a whisper of navy β€” panels now pop against this */
136
- linear-gradient(180deg, #050810 0%, #02040c 50%, #010206 100%) !important;
137
  background-size: auto, auto, auto, 32px 32px, 32px 32px, auto;
138
  background-attachment: fixed, fixed, fixed, fixed, fixed, fixed;
139
- background-color: #010206 !important;
140
  }
141
 
142
  /* ─────────────── film-grain noise overlay ─────────────── */
@@ -162,9 +240,9 @@ body::after {
162
  pointer-events: none;
163
  background: linear-gradient(90deg,
164
  transparent 5%,
165
- rgba(118, 185, 0, 0.35) 30%,
166
- rgba(118, 185, 0, 0.85) 50%,
167
- rgba(118, 185, 0, 0.35) 70%,
168
  transparent 95%);
169
  filter: blur(3px);
170
  animation: page-scanline 1.8s cubic-bezier(.4,.0,.2,1) 0.4s 1 forwards;
@@ -282,8 +360,8 @@ gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
282
  left: 50%; top: -40px;
283
  transform: translateX(-50%);
284
  background:
285
- radial-gradient(ellipse at center, rgba(95, 180, 255, 0.18) 0%, transparent 55%),
286
- radial-gradient(ellipse at 30% 70%, rgba(118, 185, 0, 0.12) 0%, transparent 60%);
287
  filter: blur(16px);
288
  pointer-events: none;
289
  z-index: -1;
@@ -337,7 +415,7 @@ gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
337
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
338
  background: var(--panel-2);
339
  box-shadow:
340
- 0 1px 0 rgba(255,255,255,0.04) inset,
341
  0 24px 60px -16px color-mix(in srgb, var(--accent) 35%, transparent);
342
  }
343
  .ds-card:hover .ds-corner { background: var(--accent); }
@@ -357,7 +435,7 @@ gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
357
  gap: 6px;
358
  border-bottom: 1px solid var(--line);
359
  background:
360
- linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.22) 100%),
361
  radial-gradient(ellipse at center, color-mix(in srgb, var(--accent) 18%, transparent) 0%, transparent 60%);
362
  overflow: hidden;
363
  color: var(--accent);
@@ -365,8 +443,8 @@ gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
365
  .ds-banner-grid {
366
  position: absolute; inset: 0;
367
  background-image:
368
- linear-gradient(rgba(150,180,240,0.06) 1px, transparent 1px),
369
- linear-gradient(90deg, rgba(150,180,240,0.06) 1px, transparent 1px);
370
  background-size: 16px 16px;
371
  pointer-events: none;
372
  mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
@@ -479,14 +557,14 @@ gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
479
  display: inline-flex; align-items: center;
480
  padding: 4px 9px;
481
  border: 1px solid var(--line);
482
- background: rgba(140, 180, 240, 0.04);
483
  font-family: var(--font-sans); font-size: 11px;
484
  font-weight: 500;
485
  letter-spacing: 0;
486
  color: var(--text-2);
487
  white-space: nowrap;
488
  }
489
- .ds-card:hover .ds-use { border-color: rgba(140, 180, 240, 0.20); }
490
 
491
  /* License chip: separate from availability status, lives at card bottom */
492
  .ds-license {
@@ -494,7 +572,7 @@ gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
494
  padding: 8px 12px;
495
  display: flex; align-items: center; gap: 10px;
496
  border: 1px solid var(--line);
497
- background: rgba(140, 180, 240, 0.03);
498
  font-family: var(--font-mono); font-size: 10px;
499
  letter-spacing: 0.04em;
500
  }
@@ -504,12 +582,12 @@ gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
504
  }
505
  .ds-license-v { color: var(--text); }
506
  .ds-license-warn {
507
- border-color: rgba(246, 200, 97, 0.30);
508
  background: var(--warn-soft);
509
  }
510
  .ds-license-warn .ds-license-v { color: var(--warn); }
511
  .ds-license-ok {
512
- border-color: rgba(118, 185, 0, 0.25);
513
  }
514
  .ds-license-ok .ds-license-v { color: var(--text); }
515
 
@@ -579,9 +657,9 @@ gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
579
  cursor: pointer !important;
580
  transition: all 200ms ease !important;
581
  }
582
- .ds-cta-ct button:hover { border-color: var(--ct) !important; background: rgba(118, 185, 0, 0.10) !important; color: var(--ct) !important; }
583
- .ds-cta-mr button:hover { border-color: var(--mr) !important; background: rgba(95, 180, 255, 0.10) !important; color: var(--mr) !important; }
584
- .ds-cta-mrb button:hover { border-color: var(--mrb) !important; background: rgba(180, 138, 255, 0.10) !important; color: var(--mrb) !important; }
585
 
586
  /* ─────────────── workspace shell ─────────────── */
587
  .workspace { padding: 0 0 32px; animation: fadein 0.35s ease both; }
@@ -595,7 +673,7 @@ gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
595
  padding: 24px 26px;
596
  margin: 0 0 28px !important;
597
  background:
598
- linear-gradient(180deg, rgba(120,165,255,0.05), rgba(120,165,255,0.02)),
599
  var(--panel);
600
  border: 1px solid var(--line);
601
  border-left: 3px solid var(--accent, var(--green));
@@ -849,7 +927,7 @@ body ul[role="listbox"] {
849
  background-color: var(--panel-2) !important;
850
  border: 1px solid var(--line-strong) !important;
851
  border-radius: 0 !important;
852
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.7), 0 2px 0 rgba(118, 185, 0, 0.20) inset !important;
853
  font-family: var(--font-sans) !important;
854
  color: var(--text) !important;
855
  padding: 4px 0 !important;
@@ -878,12 +956,12 @@ body [role="option"] {
878
  body .options li:hover,
879
  body ul[role="listbox"] li:hover,
880
  body [role="option"]:hover {
881
- background: rgba(118, 185, 0, 0.12) !important;
882
  color: var(--green) !important;
883
  }
884
  .gradio-container [role="option"][aria-selected="true"],
885
  body [role="option"][aria-selected="true"] {
886
- background: rgba(118, 185, 0, 0.18) !important;
887
  color: var(--green) !important;
888
  }
889
 
@@ -984,7 +1062,7 @@ body [role="option"][aria-selected="true"] {
984
  }
985
  .controls .gradio-checkboxgroup label:has(input:checked),
986
  .controls fieldset label:has(input:checked) {
987
- background: rgba(118, 185, 0, 0.14) !important;
988
  border-color: var(--green) !important;
989
  color: var(--green) !important;
990
  }
@@ -1066,28 +1144,28 @@ body [role="option"][aria-selected="true"] {
1066
  .controls .primary-cta button:disabled:active,
1067
  .gradio-container .primary-cta button:disabled,
1068
  .gradio-container .primary-cta button:disabled:hover {
1069
- background: linear-gradient(180deg, #4a5f00 0%, #3a4c00 100%) !important;
1070
- color: rgba(255, 255, 255, 0.70) !important;
1071
- -webkit-text-fill-color: rgba(255, 255, 255, 0.70) !important;
1072
  cursor: wait !important;
1073
  opacity: 1 !important;
1074
  transform: none !important;
1075
  box-shadow: none !important;
1076
- border-color: #2a3700 !important;
1077
  }
1078
  /* Make sure nested spans inside the disabled button inherit the dim text color */
1079
  .primary-cta button:disabled *,
1080
  .primary-cta button:disabled:hover * {
1081
- color: rgba(255, 255, 255, 0.70) !important;
1082
- -webkit-text-fill-color: rgba(255, 255, 255, 0.70) !important;
1083
  }
1084
  .primary-cta button:disabled::after {
1085
  content: "";
1086
  display: inline-block;
1087
  width: 10px; height: 10px;
1088
  margin-left: 10px;
1089
- border: 2px solid rgba(255,255,255,0.3);
1090
- border-top-color: rgba(255,255,255,0.85);
1091
  border-radius: 50%;
1092
  animation: btn-spin 0.8s linear infinite;
1093
  vertical-align: -2px;
@@ -1108,7 +1186,7 @@ body [role="option"][aria-selected="true"] {
1108
  .viewer-strip-left { color: var(--muted); }
1109
  .viewer-strip-right { color: var(--text-2); }
1110
  .viewer {
1111
- background: var(--bg-1) !important;
1112
  border: 1px solid var(--line) !important;
1113
  border-radius: 0 !important;
1114
  padding: 0 !important;
@@ -1124,7 +1202,7 @@ body [role="option"][aria-selected="true"] {
1124
  margin: 14px 0 !important;
1125
  border: 1px solid var(--line) !important;
1126
  background:
1127
- linear-gradient(180deg, rgba(80, 150, 240, 0.10) 0%, rgba(80, 150, 240, 0.03) 100%),
1128
  var(--panel) !important;
1129
  position: relative;
1130
  }
@@ -1132,14 +1210,14 @@ body [role="option"][aria-selected="true"] {
1132
  /* Thin left-edge accent to mark this as a control surface */
1133
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
1134
  width: 2px;
1135
- background: linear-gradient(180deg, rgba(95, 180, 255, 0.55), rgba(95, 180, 255, 0.15));
1136
  }
1137
  .preset-label {
1138
  font-family: var(--font-mono) !important;
1139
  font-size: 10px;
1140
  letter-spacing: 0.20em;
1141
  text-transform: uppercase;
1142
- color: rgba(170, 200, 255, 0.85);
1143
  padding: 0 !important;
1144
  }
1145
 
@@ -1159,7 +1237,7 @@ body [role="option"][aria-selected="true"] {
1159
  }
1160
  .preset-row input[type="radio"] { display: none; }
1161
  .preset-row label:has(input:checked) {
1162
- background: rgba(118, 185, 0, 0.14) !important;
1163
  border-color: var(--green) !important;
1164
  color: var(--green) !important;
1165
  }
@@ -1194,7 +1272,7 @@ body [role="option"][aria-selected="true"] {
1194
  display: inline-flex; align-items: baseline; gap: 6px;
1195
  padding: 3px 8px;
1196
  border: 1px solid var(--line);
1197
- background: rgba(140, 180, 240, 0.04);
1198
  font-family: var(--font-mono); font-size: 10px;
1199
  white-space: nowrap;
1200
  }
@@ -1206,7 +1284,7 @@ body [role="option"][aria-selected="true"] {
1206
  .license-banner {
1207
  margin: 0 0 28px !important;
1208
  padding: 14px 18px 16px;
1209
- border: 1px solid rgba(246, 200, 97, 0.25);
1210
  border-left: 3px solid var(--warn);
1211
  background: var(--warn-soft);
1212
  color: var(--warn);
@@ -1224,16 +1302,18 @@ body [role="option"][aria-selected="true"] {
1224
  font-size: 10px;
1225
  font-weight: 600;
1226
  }
1227
- .license-banner a { color: #ffe9a3; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
1228
 
1229
  /* ─────────────── empty viewport: 2x2 wireframe preview of what's coming ─────────────── */
 
 
1230
  .nv-empty {
1231
  position: relative;
1232
  width: 100%; aspect-ratio: 1/1; max-height: 720px;
1233
  background:
1234
  radial-gradient(circle at 50% 50%, rgba(95, 180, 255, 0.06) 0%, transparent 55%),
1235
- var(--bg-1);
1236
- color: var(--muted);
1237
  overflow: hidden;
1238
  }
1239
  /* The 2x2 grid showing where each MPR pane will render after generation */
@@ -1280,7 +1360,7 @@ body [role="option"][aria-selected="true"] {
1280
  text-align: center;
1281
  padding: 12px 18px;
1282
  background: linear-gradient(180deg, rgba(10, 21, 48, 0.92), rgba(10, 21, 48, 0.82));
1283
- border: 1px solid var(--line);
1284
  backdrop-filter: blur(6px);
1285
  z-index: 2;
1286
  min-width: 280px;
@@ -1289,13 +1369,13 @@ body [role="option"][aria-selected="true"] {
1289
  font-family: var(--font-sans);
1290
  font-size: 13px;
1291
  font-weight: 500;
1292
- color: var(--text);
1293
  margin-bottom: 6px;
1294
  }
1295
  .nv-empty-msg {
1296
  font-family: var(--font-sans);
1297
  font-size: 11.5px;
1298
- color: var(--muted);
1299
  max-width: 280px; text-align: center;
1300
  line-height: 1.5;
1301
  }
@@ -1425,30 +1505,13 @@ footer { display: none !important; }
1425
  """
1426
 
1427
 
1428
- # The UI is dark-mode-native (navy "MAISI Console" aesthetic) β€” there is no
1429
- # light variant. Rather than fight Gradio's light/dark toggle at the DOM level
1430
- # (DOM/observer hacks break SSR hydration), we collapse the theme itself:
1431
- # Gradio's _get_theme_css() emits every token twice β€” under `:root` (light) and
1432
- # `:root.dark` (dark). By copying each token's dark value onto its light
1433
- # counterpart, `:root` carries dark colors too, so the app renders dark
1434
- # regardless of OS preference or the HF Spaces theme switch. Pure CSS output β€”
1435
- # nothing client-side to break.
1436
- def _all_dark_theme() -> gr.themes.Base:
1437
- theme = gr.themes.Base()
1438
- for attr in list(theme.__dict__):
1439
- if attr.startswith("_") or not attr.endswith("_dark"):
1440
- continue
1441
- dark_val = theme.__dict__[attr]
1442
- if dark_val is None: # None => dark inherits light; leave light as-is
1443
- continue
1444
- light_attr = attr[: -len("_dark")]
1445
- if light_attr in theme.__dict__:
1446
- theme.__dict__[light_attr] = dark_val
1447
- return theme
1448
-
1449
-
1450
- # Keep the browser's native chrome (scrollbars, form controls) dark too.
1451
- DARK_HEAD = '<meta name="color-scheme" content="dark">'
1452
 
1453
 
1454
  def build_app() -> gr.Blocks:
@@ -1484,6 +1547,6 @@ if __name__ == "__main__":
1484
  server_port=int(os.environ.get("PORT", "7860")),
1485
  show_error=True,
1486
  css=CSS,
1487
- theme=_all_dark_theme(),
1488
- head=DARK_HEAD,
1489
  )
 
63
  CSS = """
64
  @import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap');
65
 
66
+ /* ═══════════════ theme tokens ═══════════════
67
+ Two coordinated palettes. Gradio toggles a `.dark` class on the document
68
+ from the visitor's OS / HF Spaces preference and emits its own tokens under
69
+ `:root` (light) + `:root.dark, :root .dark` (dark) β€” see gradio/themes/
70
+ base.py. We mirror that exact selector pattern so the whole UI flips with
71
+ it. `:root` carries the light "clinical blue" theme; `:root.dark` carries
72
+ the navy "MAISI Console" theme (values unchanged from the original dark
73
+ build). Accent-tinted fills use color-mix() against an accent token, which
74
+ resolves pixel-exact in dark (e.g. --green #76b900 == rgb(118,185,0)) and
75
+ adapts automatically in light. */
76
  :root {
77
+ /* fonts + layout β€” theme-independent */
78
+ --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
79
+ --font-mono: "Geist Mono", ui-monospace, "JetBrains Mono", "SF Mono", monospace;
80
+ --num: "Geist Mono", ui-monospace, monospace;
81
+ --container: 1240px;
82
+ --gutter: 32px;
83
+
84
+ /* ─── LIGHT β€” clean medical-blue clinical ─── */
85
+ --page-bg: #eef2f7;
86
+ --page-grad-a: #f7f9fc;
87
+ --page-grad-b: #eef2f8;
88
+ --page-grad-c: #e8edf5;
89
+ --bg-0: #e8edf5;
90
+ --bg-1: #f3f6fb;
91
+ --bg-2: #e6ecf5;
92
+ --panel: #ffffff;
93
+ --panel-2: #f4f7fb;
94
+ --viewer-bg: #0b1020;
95
+ --line: rgba(20, 55, 110, 0.14);
96
+ --line-strong: rgba(20, 55, 110, 0.22);
97
+ --line-bright: rgba(20, 55, 110, 0.34);
98
+ --text: #122036;
99
+ --text-2: #3b4c66;
100
+ --muted: #647387;
101
+ --muted-2: #97a3b6;
102
+
103
+ --green: #5f9400;
104
+ --green-glow: rgba(95, 148, 0, 0.40);
105
+ --green-soft: rgba(95, 148, 0, 0.12);
106
+ --warn: #9a6b00;
107
+ --warn-soft: rgba(154, 107, 0, 0.10);
108
+
109
+ --ct: #5f9400;
110
+ --mr: #1f7ed4;
111
+ --mrb: #7c4dd6;
112
+
113
+ --grid-line: rgba(20, 55, 110, 0.07);
114
+ --glow-blue: rgba(31, 126, 212, 0.07);
115
+ --glow-purple: rgba(124, 77, 214, 0.05);
116
+ --card-hl: rgba(255, 255, 255, 0);
117
+ --banner-shade: rgba(20, 40, 80, 0.06);
118
+ --banner-grid: rgba(20, 55, 110, 0.05);
119
+ --chip-bg: rgba(20, 55, 110, 0.04);
120
+ --chip-bg-faint: rgba(20, 55, 110, 0.025);
121
+ --shadow: rgba(20, 40, 85, 0.16);
122
+ --preset-label: #4a6f9e;
123
+ --license-link: #8a5200;
124
+ --cta-off-a: #c4cdbb;
125
+ --cta-off-b: #b4bdab;
126
+ --cta-off-border: #a6b39a;
127
+ --cta-off-text: rgba(24, 32, 12, 0.62);
128
+ }
129
+
130
+ :root.dark, :root .dark {
131
+ /* ─── DARK β€” navy "MAISI Console" (unchanged from original build) ─── */
132
+ --page-bg: #010206;
133
+ --page-grad-a: #050810;
134
+ --page-grad-b: #02040c;
135
+ --page-grad-c: #010206;
136
  --bg-0: #06102a;
137
  --bg-1: #0a1530;
138
  --bg-2: #0e1838;
139
  --panel: #0e1b3a;
140
  --panel-2: #142348;
141
+ --viewer-bg: #0b1020;
142
  --line: rgba(140, 180, 240, 0.12);
143
  --line-strong: rgba(140, 180, 240, 0.22);
144
  --line-bright: rgba(140, 180, 240, 0.38);
 
157
  --mr: #5fb4ff;
158
  --mrb: #b48aff;
159
 
160
+ --grid-line: rgba(0, 0, 6, 0.42);
161
+ --glow-blue: rgba(70, 140, 230, 0.16);
162
+ --glow-purple: rgba(140, 100, 220, 0.06);
163
+ --card-hl: rgba(255, 255, 255, 0.04);
164
+ --banner-shade: rgba(0, 0, 0, 0.22);
165
+ --banner-grid: rgba(150, 180, 240, 0.06);
166
+ --chip-bg: rgba(140, 180, 240, 0.04);
167
+ --chip-bg-faint: rgba(140, 180, 240, 0.03);
168
+ --shadow: rgba(0, 0, 0, 0.70);
169
+ --preset-label: rgba(170, 200, 255, 0.85);
170
+ --license-link: #ffe9a3;
171
+ --cta-off-a: #4a5f00;
172
+ --cta-off-b: #3a4c00;
173
+ --cta-off-border: #2a3700;
174
+ --cta-off-text: var(--cta-off-text);
175
  }
176
 
177
  /* ─────────────── base + blueprint grid ─────────────── */
 
192
  --color-background-primary: transparent !important;
193
  }
194
 
195
+ html { background: var(--page-bg) !important; }
196
  html, body {
197
  color: var(--text) !important;
198
  font-family: var(--font-sans);
 
202
  body {
203
  background-image:
204
  /* Concentrated blue glow behind the hero only β€” not page-wide */
205
+ radial-gradient(ellipse 1100px 600px at 50% 220px, var(--glow-blue), transparent 65%),
206
  /* NVIDIA green hint warming the top-left of the page */
207
+ radial-gradient(ellipse 700px 380px at 18% -40px, color-mix(in srgb, var(--green) 6%, transparent), transparent 70%),
208
  /* Cool purple sweep at lower-right (very faint) */
209
+ radial-gradient(ellipse 900px 600px at 108% 108%, var(--glow-purple), transparent 60%),
210
+ /* Architectural blueprint grid */
211
+ linear-gradient(var(--grid-line) 1px, transparent 1px),
212
+ linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),
213
+ /* Page base β€” subtle vertical wash so panels lift off it */
214
+ linear-gradient(180deg, var(--page-grad-a) 0%, var(--page-grad-b) 50%, var(--page-grad-c) 100%) !important;
215
  background-size: auto, auto, auto, 32px 32px, 32px 32px, auto;
216
  background-attachment: fixed, fixed, fixed, fixed, fixed, fixed;
217
+ background-color: var(--page-bg) !important;
218
  }
219
 
220
  /* ─────────────── film-grain noise overlay ─────────────── */
 
240
  pointer-events: none;
241
  background: linear-gradient(90deg,
242
  transparent 5%,
243
+ color-mix(in srgb, var(--green) 35%, transparent) 30%,
244
+ color-mix(in srgb, var(--green) 85%, transparent) 50%,
245
+ color-mix(in srgb, var(--green) 35%, transparent) 70%,
246
  transparent 95%);
247
  filter: blur(3px);
248
  animation: page-scanline 1.8s cubic-bezier(.4,.0,.2,1) 0.4s 1 forwards;
 
360
  left: 50%; top: -40px;
361
  transform: translateX(-50%);
362
  background:
363
+ radial-gradient(ellipse at center, color-mix(in srgb, var(--mr) 18%, transparent) 0%, transparent 55%),
364
+ radial-gradient(ellipse at 30% 70%, color-mix(in srgb, var(--green) 12%, transparent) 0%, transparent 60%);
365
  filter: blur(16px);
366
  pointer-events: none;
367
  z-index: -1;
 
415
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
416
  background: var(--panel-2);
417
  box-shadow:
418
+ 0 1px 0 var(--card-hl) inset,
419
  0 24px 60px -16px color-mix(in srgb, var(--accent) 35%, transparent);
420
  }
421
  .ds-card:hover .ds-corner { background: var(--accent); }
 
435
  gap: 6px;
436
  border-bottom: 1px solid var(--line);
437
  background:
438
+ linear-gradient(180deg, transparent 0%, var(--banner-shade) 100%),
439
  radial-gradient(ellipse at center, color-mix(in srgb, var(--accent) 18%, transparent) 0%, transparent 60%);
440
  overflow: hidden;
441
  color: var(--accent);
 
443
  .ds-banner-grid {
444
  position: absolute; inset: 0;
445
  background-image:
446
+ linear-gradient(var(--banner-grid) 1px, transparent 1px),
447
+ linear-gradient(90deg, var(--banner-grid) 1px, transparent 1px);
448
  background-size: 16px 16px;
449
  pointer-events: none;
450
  mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
 
557
  display: inline-flex; align-items: center;
558
  padding: 4px 9px;
559
  border: 1px solid var(--line);
560
+ background: var(--chip-bg);
561
  font-family: var(--font-sans); font-size: 11px;
562
  font-weight: 500;
563
  letter-spacing: 0;
564
  color: var(--text-2);
565
  white-space: nowrap;
566
  }
567
+ .ds-card:hover .ds-use { border-color: var(--line-strong); }
568
 
569
  /* License chip: separate from availability status, lives at card bottom */
570
  .ds-license {
 
572
  padding: 8px 12px;
573
  display: flex; align-items: center; gap: 10px;
574
  border: 1px solid var(--line);
575
+ background: var(--chip-bg-faint);
576
  font-family: var(--font-mono); font-size: 10px;
577
  letter-spacing: 0.04em;
578
  }
 
582
  }
583
  .ds-license-v { color: var(--text); }
584
  .ds-license-warn {
585
+ border-color: color-mix(in srgb, var(--warn) 30%, transparent);
586
  background: var(--warn-soft);
587
  }
588
  .ds-license-warn .ds-license-v { color: var(--warn); }
589
  .ds-license-ok {
590
+ border-color: color-mix(in srgb, var(--green) 25%, transparent);
591
  }
592
  .ds-license-ok .ds-license-v { color: var(--text); }
593
 
 
657
  cursor: pointer !important;
658
  transition: all 200ms ease !important;
659
  }
660
+ .ds-cta-ct button:hover { border-color: var(--ct) !important; background: color-mix(in srgb, var(--ct) 10%, transparent) !important; color: var(--ct) !important; }
661
+ .ds-cta-mr button:hover { border-color: var(--mr) !important; background: color-mix(in srgb, var(--mr) 10%, transparent) !important; color: var(--mr) !important; }
662
+ .ds-cta-mrb button:hover { border-color: var(--mrb) !important; background: color-mix(in srgb, var(--mrb) 10%, transparent) !important; color: var(--mrb) !important; }
663
 
664
  /* ─────────────── workspace shell ─────────────── */
665
  .workspace { padding: 0 0 32px; animation: fadein 0.35s ease both; }
 
673
  padding: 24px 26px;
674
  margin: 0 0 28px !important;
675
  background:
676
+ linear-gradient(180deg, color-mix(in srgb, var(--mr) 5%, transparent), color-mix(in srgb, var(--mr) 2%, transparent)),
677
  var(--panel);
678
  border: 1px solid var(--line);
679
  border-left: 3px solid var(--accent, var(--green));
 
927
  background-color: var(--panel-2) !important;
928
  border: 1px solid var(--line-strong) !important;
929
  border-radius: 0 !important;
930
+ box-shadow: 0 10px 40px var(--shadow), 0 2px 0 color-mix(in srgb, var(--green) 20%, transparent) inset !important;
931
  font-family: var(--font-sans) !important;
932
  color: var(--text) !important;
933
  padding: 4px 0 !important;
 
956
  body .options li:hover,
957
  body ul[role="listbox"] li:hover,
958
  body [role="option"]:hover {
959
+ background: color-mix(in srgb, var(--green) 12%, transparent) !important;
960
  color: var(--green) !important;
961
  }
962
  .gradio-container [role="option"][aria-selected="true"],
963
  body [role="option"][aria-selected="true"] {
964
+ background: color-mix(in srgb, var(--green) 18%, transparent) !important;
965
  color: var(--green) !important;
966
  }
967
 
 
1062
  }
1063
  .controls .gradio-checkboxgroup label:has(input:checked),
1064
  .controls fieldset label:has(input:checked) {
1065
+ background: color-mix(in srgb, var(--green) 14%, transparent) !important;
1066
  border-color: var(--green) !important;
1067
  color: var(--green) !important;
1068
  }
 
1144
  .controls .primary-cta button:disabled:active,
1145
  .gradio-container .primary-cta button:disabled,
1146
  .gradio-container .primary-cta button:disabled:hover {
1147
+ background: linear-gradient(180deg, var(--cta-off-a) 0%, var(--cta-off-b) 100%) !important;
1148
+ color: var(--cta-off-text) !important;
1149
+ -webkit-text-fill-color: var(--cta-off-text) !important;
1150
  cursor: wait !important;
1151
  opacity: 1 !important;
1152
  transform: none !important;
1153
  box-shadow: none !important;
1154
+ border-color: var(--cta-off-border) !important;
1155
  }
1156
  /* Make sure nested spans inside the disabled button inherit the dim text color */
1157
  .primary-cta button:disabled *,
1158
  .primary-cta button:disabled:hover * {
1159
+ color: var(--cta-off-text) !important;
1160
+ -webkit-text-fill-color: var(--cta-off-text) !important;
1161
  }
1162
  .primary-cta button:disabled::after {
1163
  content: "";
1164
  display: inline-block;
1165
  width: 10px; height: 10px;
1166
  margin-left: 10px;
1167
+ border: 2px solid color-mix(in srgb, currentColor 32%, transparent);
1168
+ border-top-color: currentColor;
1169
  border-radius: 50%;
1170
  animation: btn-spin 0.8s linear infinite;
1171
  vertical-align: -2px;
 
1186
  .viewer-strip-left { color: var(--muted); }
1187
  .viewer-strip-right { color: var(--text-2); }
1188
  .viewer {
1189
+ background: var(--viewer-bg) !important;
1190
  border: 1px solid var(--line) !important;
1191
  border-radius: 0 !important;
1192
  padding: 0 !important;
 
1202
  margin: 14px 0 !important;
1203
  border: 1px solid var(--line) !important;
1204
  background:
1205
+ linear-gradient(180deg, color-mix(in srgb, var(--mr) 10%, transparent) 0%, color-mix(in srgb, var(--mr) 3%, transparent) 100%),
1206
  var(--panel) !important;
1207
  position: relative;
1208
  }
 
1210
  /* Thin left-edge accent to mark this as a control surface */
1211
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
1212
  width: 2px;
1213
+ background: linear-gradient(180deg, color-mix(in srgb, var(--mr) 55%, transparent), color-mix(in srgb, var(--mr) 15%, transparent));
1214
  }
1215
  .preset-label {
1216
  font-family: var(--font-mono) !important;
1217
  font-size: 10px;
1218
  letter-spacing: 0.20em;
1219
  text-transform: uppercase;
1220
+ color: var(--preset-label);
1221
  padding: 0 !important;
1222
  }
1223
 
 
1237
  }
1238
  .preset-row input[type="radio"] { display: none; }
1239
  .preset-row label:has(input:checked) {
1240
+ background: color-mix(in srgb, var(--green) 14%, transparent) !important;
1241
  border-color: var(--green) !important;
1242
  color: var(--green) !important;
1243
  }
 
1272
  display: inline-flex; align-items: baseline; gap: 6px;
1273
  padding: 3px 8px;
1274
  border: 1px solid var(--line);
1275
+ background: var(--chip-bg);
1276
  font-family: var(--font-mono); font-size: 10px;
1277
  white-space: nowrap;
1278
  }
 
1284
  .license-banner {
1285
  margin: 0 0 28px !important;
1286
  padding: 14px 18px 16px;
1287
+ border: 1px solid color-mix(in srgb, var(--warn) 25%, transparent);
1288
  border-left: 3px solid var(--warn);
1289
  background: var(--warn-soft);
1290
  color: var(--warn);
 
1302
  font-size: 10px;
1303
  font-weight: 600;
1304
  }
1305
+ .license-banner a { color: var(--license-link); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
1306
 
1307
  /* ─────────────── empty viewport: 2x2 wireframe preview of what's coming ─────────────── */
1308
+ /* The viewer surface (and this placeholder) stay dark in both themes β€”
1309
+ radiology/PACS convention; grayscale volumes read most accurately on dark. */
1310
  .nv-empty {
1311
  position: relative;
1312
  width: 100%; aspect-ratio: 1/1; max-height: 720px;
1313
  background:
1314
  radial-gradient(circle at 50% 50%, rgba(95, 180, 255, 0.06) 0%, transparent 55%),
1315
+ var(--viewer-bg);
1316
+ color: #7280a0;
1317
  overflow: hidden;
1318
  }
1319
  /* The 2x2 grid showing where each MPR pane will render after generation */
 
1360
  text-align: center;
1361
  padding: 12px 18px;
1362
  background: linear-gradient(180deg, rgba(10, 21, 48, 0.92), rgba(10, 21, 48, 0.82));
1363
+ border: 1px solid rgba(140, 180, 240, 0.12);
1364
  backdrop-filter: blur(6px);
1365
  z-index: 2;
1366
  min-width: 280px;
 
1369
  font-family: var(--font-sans);
1370
  font-size: 13px;
1371
  font-weight: 500;
1372
+ color: #ecf0fa;
1373
  margin-bottom: 6px;
1374
  }
1375
  .nv-empty-msg {
1376
  font-family: var(--font-sans);
1377
  font-size: 11.5px;
1378
+ color: #7280a0;
1379
  max-width: 280px; text-align: center;
1380
  line-height: 1.5;
1381
  }
 
1505
  """
1506
 
1507
 
1508
+ # Adaptive dual-theme: the CSS block defines both a light ("clinical blue") and
1509
+ # a dark ("MAISI Console") palette under `:root` / `:root.dark`. Gradio's stock
1510
+ # Base theme toggles the `.dark` class from the visitor's OS / HF Spaces
1511
+ # preference, so the whole UI follows along β€” no DOM hacks, no force-dark.
1512
+ # `color-scheme: light dark` keeps native browser chrome (scrollbars, form
1513
+ # controls) in step with the active theme.
1514
+ THEME_HEAD = '<meta name="color-scheme" content="light dark">'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1515
 
1516
 
1517
  def build_app() -> gr.Blocks:
 
1547
  server_port=int(os.environ.get("PORT", "7860")),
1548
  show_error=True,
1549
  css=CSS,
1550
+ theme=gr.themes.Base(),
1551
+ head=THEME_HEAD,
1552
  )
ui/workspace_ct.py CHANGED
@@ -32,7 +32,7 @@ def build(spaces_gpu: Any) -> tuple[gr.Group, gr.Button]:
32
  back_btn = gr.Button("← Back", elem_classes=["back-btn"], scale=0)
33
  gr.HTML(
34
  '<div class="workspace-title">'
35
- '<span class="ws-dot" style="background:#76b900;color:#76b900"></span>'
36
  '<span class="ws-crumb">NV-Generate</span>'
37
  '<span class="ws-crumb-sep">/</span>'
38
  '<span class="ws-active">CT</span>'
 
32
  back_btn = gr.Button("← Back", elem_classes=["back-btn"], scale=0)
33
  gr.HTML(
34
  '<div class="workspace-title">'
35
+ '<span class="ws-dot" style="background:var(--ct);color:var(--ct)"></span>'
36
  '<span class="ws-crumb">NV-Generate</span>'
37
  '<span class="ws-crumb-sep">/</span>'
38
  '<span class="ws-active">CT</span>'
ui/workspace_mr.py CHANGED
@@ -21,7 +21,7 @@ def build(spaces_gpu: Any) -> tuple[gr.Group, gr.Button]:
21
  back_btn = gr.Button("← Back", elem_classes=["back-btn"], scale=0)
22
  gr.HTML(
23
  '<div class="workspace-title">'
24
- '<span class="ws-dot" style="background:#5fb4ff;color:#5fb4ff"></span>'
25
  '<span class="ws-crumb">NV-Generate</span>'
26
  '<span class="ws-crumb-sep">/</span>'
27
  '<span class="ws-active">MR</span>'
 
21
  back_btn = gr.Button("← Back", elem_classes=["back-btn"], scale=0)
22
  gr.HTML(
23
  '<div class="workspace-title">'
24
+ '<span class="ws-dot" style="background:var(--mr);color:var(--mr)"></span>'
25
  '<span class="ws-crumb">NV-Generate</span>'
26
  '<span class="ws-crumb-sep">/</span>'
27
  '<span class="ws-active">MR</span>'
ui/workspace_mr_brain.py CHANGED
@@ -17,7 +17,7 @@ def build(spaces_gpu: Any) -> tuple[gr.Group, gr.Button]:
17
  back_btn = gr.Button("← Back", elem_classes=["back-btn"], scale=0)
18
  gr.HTML(
19
  '<div class="workspace-title">'
20
- '<span class="ws-dot" style="background:#b48aff;color:#b48aff"></span>'
21
  '<span class="ws-crumb">NV-Generate</span>'
22
  '<span class="ws-crumb-sep">/</span>'
23
  '<span class="ws-active">MR Brain</span>'
 
17
  back_btn = gr.Button("← Back", elem_classes=["back-btn"], scale=0)
18
  gr.HTML(
19
  '<div class="workspace-title">'
20
+ '<span class="ws-dot" style="background:var(--mrb);color:var(--mrb)"></span>'
21
  '<span class="ws-crumb">NV-Generate</span>'
22
  '<span class="ws-crumb-sep">/</span>'
23
  '<span class="ws-active">MR Brain</span>'