Spaces:
Running on Zero
Add adaptive clinical dual-theme (light + dark)
Browse filesReplaces 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>
- app.py +149 -86
- ui/workspace_ct.py +1 -1
- ui/workspace_mr.py +1 -1
- ui/workspace_mr_brain.py +1 -1
|
@@ -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 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
--
|
| 92 |
-
--
|
| 93 |
-
--
|
| 94 |
-
|
| 95 |
-
--
|
| 96 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
|
| 99 |
/* βββββββββββββββ base + blueprint grid βββββββββββββββ */
|
|
@@ -114,7 +192,7 @@ gradio-app {
|
|
| 114 |
--color-background-primary: transparent !important;
|
| 115 |
}
|
| 116 |
|
| 117 |
-
html { background:
|
| 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,
|
| 128 |
/* NVIDIA green hint warming the top-left of the page */
|
| 129 |
-
radial-gradient(ellipse 700px 380px at 18% -40px,
|
| 130 |
/* Cool purple sweep at lower-right (very faint) */
|
| 131 |
-
radial-gradient(ellipse 900px 600px at 108% 108%,
|
| 132 |
-
/* Architectural blueprint grid
|
| 133 |
-
linear-gradient(
|
| 134 |
-
linear-gradient(90deg,
|
| 135 |
-
/*
|
| 136 |
-
linear-gradient(180deg,
|
| 137 |
background-size: auto, auto, auto, 32px 32px, 32px 32px, auto;
|
| 138 |
background-attachment: fixed, fixed, fixed, fixed, fixed, fixed;
|
| 139 |
-
background-color:
|
| 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 |
-
|
| 166 |
-
|
| 167 |
-
|
| 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,
|
| 286 |
-
radial-gradient(ellipse at 30% 70%,
|
| 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
|
| 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%,
|
| 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(
|
| 369 |
-
linear-gradient(90deg,
|
| 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:
|
| 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:
|
| 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:
|
| 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:
|
| 508 |
background: var(--warn-soft);
|
| 509 |
}
|
| 510 |
.ds-license-warn .ds-license-v { color: var(--warn); }
|
| 511 |
.ds-license-ok {
|
| 512 |
-
border-color:
|
| 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:
|
| 583 |
-
.ds-cta-mr button:hover { border-color: var(--mr) !important; background:
|
| 584 |
-
.ds-cta-mrb button:hover { border-color: var(--mrb) !important; background:
|
| 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,
|
| 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
|
| 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:
|
| 882 |
color: var(--green) !important;
|
| 883 |
}
|
| 884 |
.gradio-container [role="option"][aria-selected="true"],
|
| 885 |
body [role="option"][aria-selected="true"] {
|
| 886 |
-
background:
|
| 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:
|
| 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,
|
| 1070 |
-
color:
|
| 1071 |
-
-webkit-text-fill-color:
|
| 1072 |
cursor: wait !important;
|
| 1073 |
opacity: 1 !important;
|
| 1074 |
transform: none !important;
|
| 1075 |
box-shadow: none !important;
|
| 1076 |
-
border-color:
|
| 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:
|
| 1082 |
-
-webkit-text-fill-color:
|
| 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
|
| 1090 |
-
border-top-color:
|
| 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(--
|
| 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,
|
| 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,
|
| 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:
|
| 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:
|
| 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:
|
| 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
|
| 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:
|
| 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(--
|
| 1236 |
-
color:
|
| 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
|
| 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:
|
| 1293 |
margin-bottom: 6px;
|
| 1294 |
}
|
| 1295 |
.nv-empty-msg {
|
| 1296 |
font-family: var(--font-sans);
|
| 1297 |
font-size: 11.5px;
|
| 1298 |
-
color:
|
| 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 |
-
#
|
| 1429 |
-
#
|
| 1430 |
-
#
|
| 1431 |
-
#
|
| 1432 |
-
# `:
|
| 1433 |
-
#
|
| 1434 |
-
|
| 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=
|
| 1488 |
-
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 |
)
|
|
@@ -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:
|
| 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>'
|
|
@@ -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:
|
| 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>'
|
|
@@ -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:
|
| 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>'
|