nv-generate / app.py
zephyrie's picture
Tune ZeroGPU durations: CT 90s, MR/MR-Brain 60s
57d05e1
"""
NV-Generate Showcase β€” Gradio app entrypoint.
Hero card landing β†’ per-model workspace. Each model's Generate button is wrapped
with @spaces.GPU on Hugging Face Spaces (ZeroGPU); locally the decorator is a no-op.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
import gradio as gr
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT))
def _ensure_upstream():
"""Clone the NV-Generate-CTMR inference repo on first run.
HF Spaces with `sdk: gradio` does not run shell scripts at build time, so we
bootstrap the upstream checkout here at module load. Idempotent: skips if
the directory already exists (local dev, subsequent Space restarts).
"""
upstream = ROOT / "repos" / "NV-Generate-CTMR"
if upstream.exists():
return
print("[nv-generate] cloning NV-Generate-CTMR upstream...", flush=True)
upstream.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
"git", "clone", "--depth", "1",
"https://github.com/NVIDIA-Medtech/NV-Generate-CTMR.git",
str(upstream),
],
check=True,
)
_ensure_upstream()
# ZeroGPU decorator β€” present on HF Spaces, optional locally.
try:
import spaces # type: ignore
spaces_gpu_ct = spaces.GPU(duration=90)
spaces_gpu_mr = spaces.GPU(duration=60)
spaces_gpu_mr_brain = spaces.GPU(duration=60)
except (ImportError, AttributeError):
def _identity(d=None): # noqa: ARG001
def deco(fn):
return fn
return deco
spaces_gpu_ct = _identity()
spaces_gpu_mr = _identity()
spaces_gpu_mr_brain = _identity()
from ui.hero import render_hero
from ui import workspace_ct, workspace_mr, workspace_mr_brain
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap');
/* ═══════════════ theme tokens ═══════════════
Two coordinated palettes. Gradio toggles a `.dark` class on the document
from the visitor's OS / HF Spaces preference and emits its own tokens under
`:root` (light) + `:root.dark, :root .dark` (dark) β€” see gradio/themes/
base.py. We mirror that exact selector pattern so the whole UI flips with
it. `:root` carries the light "clinical blue" theme; `:root.dark` carries
the navy "MAISI Console" theme (values unchanged from the original dark
build). Accent-tinted fills use color-mix() against an accent token, which
resolves pixel-exact in dark (e.g. --green #76b900 == rgb(118,185,0)) and
adapts automatically in light. */
:root {
/* fonts + layout β€” theme-independent */
--font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: "Geist Mono", ui-monospace, "JetBrains Mono", "SF Mono", monospace;
--num: "Geist Mono", ui-monospace, monospace;
--container: 1240px;
--gutter: 32px;
/* ─── LIGHT β€” clean medical-blue clinical ─── */
--page-bg: #eceff4;
--page-grad-a: #eff2f6;
--page-grad-b: #eceff4;
--page-grad-c: #e6eaf0;
--bg-0: #f1f5f9;
--bg-1: #ffffff;
--bg-2: #f1f5f9;
--panel: #ffffff;
--panel-2: #f1f5f9;
--viewer-bg: #0b1020;
--line: #d8e0ea;
--line-strong: #b8c4d4;
--line-bright: #9aa9bd;
--text: #0f172a;
--text-2: #334155;
--muted: #64748b;
--muted-2: #94a3b8;
--green: #5d9400;
--green-glow: rgba(93, 148, 0, 0.35);
--green-soft: rgba(93, 148, 0, 0.12);
--warn: #9a6b00;
--warn-soft: rgba(154, 107, 0, 0.10);
--ct: #5d9400;
--mr: #2563eb;
--mrb: #7c3aed;
--grid-line: rgba(15, 23, 42, 0.045);
--glow-blue: rgba(37, 99, 235, 0.07);
--glow-purple: rgba(124, 58, 237, 0.05);
--card-hl: rgba(255, 255, 255, 0);
--banner-shade: rgba(15, 23, 42, 0.05);
--banner-grid: rgba(15, 23, 42, 0.05);
--chip-bg: #f1f5f9;
--chip-bg-faint: #f8fafc;
--shadow: rgba(15, 23, 42, 0.14);
--shadow-card: 0 1px 2px rgba(15, 23, 42, 0.05), 0 12px 32px rgba(15, 23, 42, 0.08);
--shadow-card-hover: 0 2px 6px rgba(15, 23, 42, 0.07), 0 22px 48px -10px rgba(15, 23, 42, 0.16);
--preset-label: #475569;
--license-link: #8a5200;
--radius: 0;
--focus: #2563eb;
--focus-ring: 0 0 0 3px rgba(37, 99, 235, 0.18);
--corner-display: block;
--pill-hover-bg: #f1f5f9;
--slider-accent: #2563eb;
--cta-off-a: #c4cdbb;
--cta-off-b: #b4bdab;
--cta-off-border: #a6b39a;
--cta-off-text: rgba(24, 32, 12, 0.62);
}
:root.dark, :root .dark {
/* ─── DARK β€” navy "MAISI Console" (unchanged from original build) ─── */
--page-bg: #010206;
--page-grad-a: #050810;
--page-grad-b: #02040c;
--page-grad-c: #010206;
--bg-0: #06102a;
--bg-1: #0a1530;
--bg-2: #0e1838;
--panel: #0e1b3a;
--panel-2: #142348;
--viewer-bg: #0b1020;
--line: rgba(140, 180, 240, 0.12);
--line-strong: rgba(140, 180, 240, 0.22);
--line-bright: rgba(140, 180, 240, 0.38);
--text: #ecf0fa;
--text-2: #b0bcd5;
--muted: #7280a0;
--muted-2: #4a5778;
--green: #76b900;
--green-glow: rgba(118, 185, 0, 0.55);
--green-soft: rgba(118, 185, 0, 0.10);
--warn: #f6c861;
--warn-soft: rgba(246, 200, 97, 0.10);
--ct: #76b900;
--mr: #5fb4ff;
--mrb: #b48aff;
--grid-line: rgba(0, 0, 6, 0.42);
--glow-blue: rgba(70, 140, 230, 0.16);
--glow-purple: rgba(140, 100, 220, 0.06);
--card-hl: rgba(255, 255, 255, 0.04);
--banner-shade: rgba(0, 0, 0, 0.22);
--banner-grid: rgba(150, 180, 240, 0.06);
--chip-bg: rgba(140, 180, 240, 0.04);
--chip-bg-faint: rgba(140, 180, 240, 0.03);
--shadow: rgba(0, 0, 0, 0.70);
--shadow-card: none;
--shadow-card-hover: 0 1px 0 var(--card-hl) inset, 0 24px 60px -16px color-mix(in srgb, var(--accent) 35%, transparent);
--preset-label: rgba(170, 200, 255, 0.85);
--license-link: #ffe9a3;
--radius: 0;
--focus: var(--green);
--focus-ring: 0 0 0 1px var(--green-soft);
--corner-display: block;
--pill-hover-bg: transparent;
--slider-accent: var(--green);
--cta-off-a: #4a5f00;
--cta-off-b: #3a4c00;
--cta-off-border: #2a3700;
--cta-off-text: rgba(255, 255, 255, 0.70);
}
/* ─────────────── base + blueprint grid ─────────────── */
/* Override Gradio Base theme tokens that paint a default gray fill across the app */
.gradio-container,
.gradio-container *,
gradio-app {
--body-background-fill: transparent !important;
--body-background-fill-dark: transparent !important;
--background-fill-primary: transparent !important;
--background-fill-primary-dark: transparent !important;
--background-fill-secondary: transparent !important;
--background-fill-secondary-dark: transparent !important;
--block-background-fill: transparent !important;
--block-background-fill-dark: transparent !important;
--panel-background-fill: transparent !important;
--panel-background-fill-dark: transparent !important;
--color-background-primary: transparent !important;
}
html { background: var(--page-bg) !important; }
html, body {
color: var(--text) !important;
font-family: var(--font-sans);
font-feature-settings: "ss01", "cv11", "tnum";
}
body {
background-image:
/* Concentrated blue glow behind the hero only β€” not page-wide */
radial-gradient(ellipse 1100px 600px at 50% 220px, var(--glow-blue), transparent 65%),
/* NVIDIA green hint warming the top-left of the page */
radial-gradient(ellipse 700px 380px at 18% -40px, color-mix(in srgb, var(--green) 6%, transparent), transparent 70%),
/* Cool purple sweep at lower-right (very faint) */
radial-gradient(ellipse 900px 600px at 108% 108%, var(--glow-purple), transparent 60%),
/* Architectural blueprint grid */
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),
/* Page base β€” subtle vertical wash so panels lift off it */
linear-gradient(180deg, var(--page-grad-a) 0%, var(--page-grad-b) 50%, var(--page-grad-c) 100%) !important;
background-size: auto, auto, auto, 32px 32px, 32px 32px, auto;
background-attachment: fixed, fixed, fixed, fixed, fixed, fixed;
background-color: var(--page-bg) !important;
}
/* ─────────────── film-grain noise overlay ─────────────── */
/* Breaks gradient banding, adds photographic warmth across every screenshot. */
body::before {
content: "";
position: fixed; inset: 0;
z-index: 0;
pointer-events: none;
background-image: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.6 0 0 0 0 0.7 0 0 0 0 0.9 0 0 0 1 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-size: 240px 240px;
opacity: 0.045;
mix-blend-mode: overlay;
}
/* ─────────────── one-time scanline sweep on page load ─────────────── */
body::after {
content: "";
position: fixed;
left: 0; right: 0; top: -8px;
height: 2px;
z-index: 9999;
pointer-events: none;
background: linear-gradient(90deg,
transparent 5%,
color-mix(in srgb, var(--green) 35%, transparent) 30%,
color-mix(in srgb, var(--green) 85%, transparent) 50%,
color-mix(in srgb, var(--green) 35%, transparent) 70%,
transparent 95%);
filter: blur(3px);
animation: page-scanline 1.8s cubic-bezier(.4,.0,.2,1) 0.4s 1 forwards;
opacity: 0;
}
@keyframes page-scanline {
0% { transform: translateY(0); opacity: 0; }
8% { opacity: 0.9; }
50% { opacity: 1; }
90% { opacity: 0.4; }
100% { transform: translateY(100vh); opacity: 0; }
}
/* All Gradio root / wrapper elements stay transparent so body bg shows through */
gradio-app, gradio-app > div, gradio-app .main, gradio-app .wrap,
gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain {
background: transparent !important;
background-color: transparent !important;
}
.gradio-container {
max-width: var(--container) !important;
padding: 0 var(--gutter) !important;
background: transparent !important;
background-color: transparent !important;
font-family: var(--font-sans) !important;
margin: 0 auto !important;
}
/* Override Gradio's default panel chrome β€” every wrapper class that can show bg */
.gradio-container .form, .gradio-container .gap, .gradio-container .panel,
.gradio-container .block, .gradio-container .gr-box, .gradio-container .gr-group,
.gradio-container .gr-padded, .gradio-container > .main, .gradio-container > .wrap,
.gradio-container .styler, .gradio-container .container, .gradio-container .row,
.gradio-container .column {
background: transparent !important;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
/* gr.HTML wraps its value in a .html-container div with horizontal padding.
Zero the PADDING so HTML blocks (ws-intro, viewer-strip, legend, status) sit
flush against the column edge; leave margin alone so children's own
margin-bottom (e.g. .ws-intro { margin: 0 0 28px }) still creates space below. */
.gradio-container .html-container,
.gradio-container [class*="html-container"] {
padding: 0 !important;
background: transparent !important;
}
/* The hero/workspace gr.Group wrappers must not add their own chrome */
.hero, .workspace {
background: transparent !important;
background-color: transparent !important;
border: 0 !important; padding: 0 !important;
}
.hero > .gap, .workspace > .gap { padding: 0 !important; background: transparent !important; }
.dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; background: var(--green); box-shadow: 0 0 6px var(--green-glow); }
.dot-pulse { animation: dotpulse 2s ease-in-out infinite; }
@keyframes dotpulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@keyframes fadein { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
/* ─────────────── masthead ─────────────── */
.masthead {
display: flex; align-items: center; justify-content: space-between;
height: 56px;
border-bottom: 1px solid var(--line);
font-family: var(--font-sans);
animation: fadein 0.4s ease both;
}
.masthead-brand {
display: flex; align-items: center; gap: 12px;
text-decoration: none; color: var(--text);
}
.nv-mark {
display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 22px;
background: var(--green); color: #000;
font-family: var(--font-mono); font-weight: 700; font-size: 11px;
letter-spacing: 0.04em;
clip-path: polygon(0 0, 100% 0, 100% 70%, 86% 100%, 0 100%);
}
.masthead-name {
font-size: 14px; font-weight: 500; letter-spacing: -0.005em;
color: var(--text);
}
.masthead-nav {
display: flex; align-items: center; gap: 18px;
font-family: var(--font-sans);
font-size: 13px;
color: var(--text-2);
}
.masthead-nav a {
color: var(--text-2); text-decoration: none;
transition: color 160ms ease;
}
.masthead-nav a:hover { color: var(--green); }
.masthead-sep { width: 3px; height: 3px; background: var(--muted-2); border-radius: 50%; }
/* ─────────────── hero block ─────────────── */
.hero { padding: 0 0 32px; }
.hero-mono {
padding: 88px 0 48px; text-align: center;
max-width: 720px;
margin: 0 auto;
position: relative;
animation: fadein 0.55s ease 0.05s both;
}
/* Soft blue glow behind the headline gives atmospheric depth */
.hero-mono::before {
content: "";
position: absolute;
width: 720px; height: 380px;
left: 50%; top: -40px;
transform: translateX(-50%);
background:
radial-gradient(ellipse at center, color-mix(in srgb, var(--mr) 18%, transparent) 0%, transparent 55%),
radial-gradient(ellipse at 30% 70%, color-mix(in srgb, var(--green) 12%, transparent) 0%, transparent 60%);
filter: blur(16px);
pointer-events: none;
z-index: -1;
opacity: 0.9;
}
.hero-eyebrow {
display: inline-flex; align-items: center; gap: 12px;
color: var(--green);
font-family: var(--font-mono); font-size: 11px; font-weight: 500;
letter-spacing: 0.18em; text-transform: uppercase;
margin-bottom: 24px;
}
.line-tick { display: inline-block; width: 28px; height: 1px; background: var(--green); opacity: 0.5; }
.hero-title {
font-family: var(--font-sans);
font-size: clamp(2.0rem, 4.0vw, 3.0rem);
font-weight: 500;
line-height: 1.05;
letter-spacing: -0.032em;
margin: 0;
color: var(--text);
}
/* ─────────────── datasheet cards ─────────────── */
.hero-row { gap: 20px !important; align-items: stretch !important; }
.hero-row > * { display: flex !important; flex-direction: column !important; }
.hero-card-col {
padding: 0 !important;
display: flex !important; flex-direction: column !important;
animation: fadein 0.5s ease both;
}
.hero-card-col:nth-child(1) { animation-delay: 0.10s; }
.hero-card-col:nth-child(2) { animation-delay: 0.18s; }
.hero-card-col:nth-child(3) { animation-delay: 0.26s; }
.ds-card {
position: relative;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
transition: border-color 220ms ease, background 220ms ease, transform 220ms ease, box-shadow 280ms ease;
--accent: var(--green);
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
.ds-card.ds-ct { --accent: var(--ct); }
.ds-card.ds-mr { --accent: var(--mr); }
.ds-card.ds-mrb { --accent: var(--mrb); }
.ds-card:hover {
border-color: color-mix(in srgb, var(--accent) 60%, transparent);
background: var(--panel-2);
box-shadow: var(--shadow-card-hover);
}
.ds-card:hover .ds-corner { background: var(--accent); }
.ds-frame {
position: relative;
padding: 18px 22px 22px;
display: flex; flex-direction: column;
flex: 1 1 auto;
}
/* Banner at top of card β€” modality scan icon over a faint accent gradient */
.ds-banner {
position: relative;
height: 110px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 6px;
border-bottom: 1px solid var(--line);
background:
linear-gradient(180deg, transparent 0%, var(--banner-shade) 100%),
radial-gradient(ellipse at center, color-mix(in srgb, var(--accent) 18%, transparent) 0%, transparent 60%);
overflow: hidden;
color: var(--accent);
}
.ds-banner-grid {
position: absolute; inset: 0;
background-image:
linear-gradient(var(--banner-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--banner-grid) 1px, transparent 1px);
background-size: 16px 16px;
pointer-events: none;
mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
}
/* Single scanline sweep on first card render β€” telegraphs "this generates volumes" */
.ds-banner::after {
content: "";
position: absolute; left: 0; right: 0; top: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
filter: blur(1px);
opacity: 0;
animation: banner-scan 2.6s cubic-bezier(.4,0,.2,1) 1 forwards;
animation-delay: 0.5s;
}
.hero-card-col:nth-child(1) .ds-banner::after { animation-delay: 0.5s; }
.hero-card-col:nth-child(2) .ds-banner::after { animation-delay: 0.75s; }
.hero-card-col:nth-child(3) .ds-banner::after { animation-delay: 1.0s; }
@keyframes banner-scan {
0% { top: 0; opacity: 0; }
10% { opacity: 0.85; }
85% { opacity: 0.85; }
100% { top: 110px; opacity: 0; }
}
.ds-banner-icon {
width: 64px; height: 64px;
position: relative; z-index: 1;
display: flex; align-items: center; justify-content: center;
transition: transform 280ms ease;
}
.ds-card:hover .ds-banner-icon { transform: scale(1.05); }
.ds-banner-icon svg { width: 100%; height: 100%; display: block; }
.ds-banner-caption {
font-family: var(--font-mono); font-size: 9.5px;
letter-spacing: 0.16em; text-transform: uppercase;
color: var(--accent);
opacity: 0.85;
position: relative; z-index: 1;
}
.ds-corner {
position: absolute; width: 7px; height: 7px;
background: var(--line-bright);
display: var(--corner-display);
transition: background 220ms ease;
}
.ds-tl { top: -1px; left: -1px; clip-path: polygon(0 0, 100% 0, 0 100%); }
.ds-tr { top: -1px; right: -1px; clip-path: polygon(0 0, 100% 0, 100% 100%); }
.ds-bl { bottom: -1px; left: -1px; clip-path: polygon(0 0, 0 100%, 100% 100%); }
.ds-br { bottom: -1px; right: -1px; clip-path: polygon(100% 0, 100% 100%, 0 100%); }
.ds-head {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 18px;
height: 24px;
}
.ds-index {
font-family: var(--font-mono);
font-size: 11px; font-weight: 600; letter-spacing: 0.18em;
color: var(--accent);
padding: 3px 9px;
border: 1px solid var(--accent);
border-radius: var(--radius);
opacity: 0.9;
}
/* Availability status β€” single dot + label. Decoupled from license info. */
.ds-status {
display: inline-flex; align-items: center; gap: 7px;
font-family: var(--font-mono); font-size: 9.5px;
letter-spacing: 0.16em; text-transform: uppercase;
color: var(--muted);
}
.ds-status .dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
.ds-name { margin-bottom: 16px; }
.ds-name-title {
font-family: var(--font-sans);
font-size: 1.20rem; font-weight: 500;
letter-spacing: -0.018em; line-height: 1.15;
color: var(--text);
margin-bottom: 4px;
}
.ds-name-code {
font-family: var(--font-mono); font-size: 11px;
letter-spacing: 0.04em; text-transform: lowercase;
color: var(--accent);
opacity: 0.85;
margin-bottom: 10px;
}
.ds-name-sub {
font-family: var(--font-sans);
font-size: 0.86rem; line-height: 1.5;
color: var(--text-2);
min-height: 2.6em; /* reserves ~2 lines so cards stay aligned */
margin-bottom: 14px;
}
/* Use-case section label */
.ds-uses-label {
font-family: var(--font-mono); font-size: 9px;
letter-spacing: 0.22em; text-transform: uppercase;
color: var(--muted);
margin-bottom: 8px;
}
/* Use-case chips: small mono pills below the subtitle */
.ds-uses {
display: flex; flex-wrap: wrap; gap: 6px;
margin-bottom: 16px;
min-height: 28px;
}
.ds-use {
display: inline-flex; align-items: center;
padding: 4px 9px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--chip-bg);
font-family: var(--font-sans); font-size: 11px;
font-weight: 500;
letter-spacing: 0;
color: var(--text-2);
white-space: nowrap;
}
.ds-card:hover .ds-use { border-color: var(--line-strong); }
/* License chip: separate from availability status, lives at card bottom */
.ds-license {
margin-top: 14px;
padding: 8px 12px;
display: flex; align-items: center; gap: 10px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--chip-bg-faint);
font-family: var(--font-mono); font-size: 10px;
letter-spacing: 0.04em;
}
.ds-license-k {
color: var(--muted);
text-transform: uppercase; letter-spacing: 0.16em; font-size: 9px;
}
.ds-license-v { color: var(--text); }
.ds-license-warn {
border-color: color-mix(in srgb, var(--warn) 30%, transparent);
background: var(--warn-soft);
}
.ds-license-warn .ds-license-v { color: var(--warn); }
.ds-license-ok {
border-color: color-mix(in srgb, var(--green) 25%, transparent);
}
.ds-license-ok .ds-license-v { color: var(--text); }
.ds-divider {
height: 1px; background: var(--line);
margin: 0 0 14px;
position: relative;
}
.ds-divider::before, .ds-divider::after {
content: ""; position: absolute; top: -2px; width: 4px; height: 4px;
background: var(--accent);
}
.ds-divider::before { left: 0; }
.ds-divider::after { right: 0; }
.ds-spec {
display: flex; flex-direction: column; gap: 7px;
margin-top: auto; /* pushes spec to bottom of frame so cards always end at same baseline */
}
.ds-row {
display: flex; align-items: baseline;
font-family: var(--font-mono); font-size: 10.5px;
letter-spacing: 0.02em;
color: var(--text-2);
white-space: nowrap;
overflow: hidden;
}
.ds-key {
color: var(--muted);
flex-shrink: 0;
text-transform: uppercase;
font-weight: 500;
}
.ds-leader {
flex: 1 1 auto;
border-bottom: 1px dotted var(--line-strong);
margin: 0 8px;
height: 0;
align-self: end;
margin-bottom: 4px;
min-width: 12px;
}
.ds-val {
color: var(--text);
text-align: right;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
}
/* CTA buttons under each card */
.ds-cta { padding: 0 !important; margin: 0 !important; }
.ds-cta button, button.ds-cta {
width: 100% !important;
margin-top: 14px !important;
padding: 14px 16px !important;
background: transparent !important;
border: 1px solid var(--line-strong) !important;
border-radius: var(--radius) !important;
color: var(--text) !important;
font-family: var(--font-sans) !important;
font-size: 13px !important;
font-weight: 500 !important;
letter-spacing: 0 !important;
text-transform: none !important;
text-align: center !important;
cursor: pointer !important;
transition: all 200ms ease !important;
}
/* Gradio puts elem_classes directly on the <button>, so the hover rule must
target `button.ds-cta-ct` β€” not `.ds-cta-ct button` (descendant, matches
nothing). Accent border + fill + text + a slight lift make the card CTAs
clearly interactive. */
button.ds-cta-ct:hover,
button.ds-cta-mr:hover,
button.ds-cta-mrb:hover {
transform: translateY(-1px) !important;
}
button.ds-cta-ct:hover { border-color: var(--ct) !important; background: color-mix(in srgb, var(--ct) 16%, transparent) !important; color: var(--ct) !important; box-shadow: 0 6px 18px -6px color-mix(in srgb, var(--ct) 50%, transparent) !important; }
button.ds-cta-mr:hover { border-color: var(--mr) !important; background: color-mix(in srgb, var(--mr) 16%, transparent) !important; color: var(--mr) !important; box-shadow: 0 6px 18px -6px color-mix(in srgb, var(--mr) 50%, transparent) !important; }
button.ds-cta-mrb:hover { border-color: var(--mrb) !important; background: color-mix(in srgb, var(--mrb) 16%, transparent) !important; color: var(--mrb) !important; box-shadow: 0 6px 18px -6px color-mix(in srgb, var(--mrb) 50%, transparent) !important; }
/* ─────────────── workspace shell ─────────────── */
.workspace { padding: 0 0 32px; animation: fadein 0.35s ease both; }
.workspace-row { gap: 24px !important; align-items: stretch !important; }
/* ─────────────── workspace intro (per-model context block) ─────────────── */
/* The gr.HTML .block wrapper around the intro card sets an inline
margin-bottom: 20px. Override it (inline styles need !important) so the
card clears the controls sidebar / viewer below. Targeted via :has() so it
only hits the intro block. Theme-independent β€” identical in light + dark. */
.gradio-container .block:has(.ws-intro) { margin-bottom: 28px !important; }
.ws-intro {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
gap: 32px;
padding: 24px 26px;
margin: 0 0 28px !important;
background:
linear-gradient(180deg, color-mix(in srgb, var(--mr) 5%, transparent), color-mix(in srgb, var(--mr) 2%, transparent)),
var(--panel);
border: 1px solid var(--line);
border-left: 3px solid var(--accent, var(--green));
border-radius: var(--radius);
box-shadow: var(--shadow-card);
position: relative;
--accent: var(--green);
}
.ws-intro-ct { --accent: var(--ct); }
.ws-intro-mr { --accent: var(--mr); }
.ws-intro-mrb { --accent: var(--mrb); }
.ws-intro-title {
font-family: var(--font-sans);
font-size: 1.30rem; font-weight: 500;
letter-spacing: -0.018em; line-height: 1.15;
color: var(--text);
margin: 0 0 10px;
}
.ws-intro-desc {
font-family: var(--font-sans);
font-size: 0.95rem; line-height: 1.55;
color: var(--text-2);
margin: 0;
max-width: 56ch;
}
.ws-intro-facts {
display: flex; flex-direction: column;
gap: 6px;
align-self: center;
}
.ws-fact {
display: flex; align-items: baseline; gap: 12px;
font-family: var(--font-mono); font-size: 11px;
letter-spacing: 0.02em;
}
.ws-fact-k {
color: var(--accent, var(--green));
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 9.5px;
flex: 0 0 92px;
}
.ws-fact-v { color: var(--text); }
.ws-fact-v.ws-fact-warn { color: var(--warn); }
.workspace-header {
align-items: center !important;
gap: 14px !important;
padding: 18px 0 18px !important;
border-bottom: 1px solid var(--line);
margin-bottom: 22px;
}
.workspace-title {
font-family: var(--font-sans);
font-size: 14px;
color: var(--text-2);
display: flex; align-items: center; gap: 10px;
flex: 1;
}
.workspace-title .ws-dot { width: 7px; height: 7px; border-radius: 50%; box-shadow: 0 0 6px currentColor; flex-shrink: 0; }
.workspace-title .ws-crumb { color: var(--text-2); font-weight: 500; }
.workspace-title .ws-crumb-sep { color: var(--muted-2); margin: 0 2px; }
.workspace-title .ws-active { color: var(--text); font-weight: 500; }
.workspace-title .ws-meta {
font-family: var(--font-mono); font-size: 11px;
color: var(--muted); margin-left: 14px;
padding: 3px 8px; border: 1px solid var(--line);
letter-spacing: 0.04em;
}
.back-btn { padding: 0 !important; margin: 0 !important; flex: 0 0 auto !important; }
.back-btn button {
background: transparent !important;
border: 1px solid var(--line-strong) !important;
border-radius: var(--radius) !important;
color: var(--text-2) !important;
font-family: var(--font-sans) !important;
font-size: 13px !important;
font-weight: 500 !important;
letter-spacing: 0 !important;
text-transform: none !important;
padding: 8px 14px !important;
}
.back-btn button:hover { border-color: var(--line-bright) !important; color: var(--text) !important; }
.hint {
font-family: var(--font-sans); font-size: 12px;
color: var(--muted);
padding: 4px 0 6px;
margin: -4px 0 0;
}
/* ─────────────── controls panel ─────────────── */
/* Named panels carry their own surface β€” the `.gradio-container` prefix lifts
their specificity above the wrapper-chrome reset block, which otherwise
strips background / border / shadow off Gradio-wrapped blocks. */
.gradio-container .controls {
background: var(--panel) !important;
border: 1px solid var(--line) !important;
border-radius: var(--radius) !important;
box-shadow: var(--shadow-card) !important;
padding: 18px 22px !important;
position: relative;
}
/* Reset Gradio's interior block padding so we control it. The per-field
`.block` wrapper carries a 12px horizontal padding that pushed inputs and
field labels ~12px right of the pill rows / section headers; zero its
left/right padding so every control shares the panel's content-left edge. */
.controls > * { padding: 0 !important; }
.controls .block {
padding-left: 0 !important;
padding-right: 0 !important;
}
/* Section headers (the gr.Markdown ##### lines) */
.controls h5 {
font-family: var(--font-mono) !important;
font-size: 10px !important;
font-weight: 600 !important;
letter-spacing: 0.20em !important;
text-transform: uppercase !important;
color: var(--green) !important;
margin: 24px 0 14px !important;
padding: 0 0 10px !important;
border-bottom: 1px solid var(--line) !important;
display: flex; align-items: center; gap: 10px;
}
.controls h5::before {
content: ""; width: 4px; height: 4px; background: var(--green);
display: inline-block;
}
/* First section header sits flush at top of panel */
.controls > div:first-child h5,
.controls .markdown:first-child h5 { margin-top: 0 !important; }
/* Input labels: title case sans, not shouting mono */
.controls label, .controls label > span {
font-family: var(--font-sans) !important;
font-size: 12px !important;
font-weight: 500 !important;
letter-spacing: 0 !important;
text-transform: none !important;
color: var(--text-2) !important;
}
/* Helper text under input (info=) */
.controls .info, .controls .gr-info, .controls .help-text {
font-family: var(--font-sans) !important;
font-size: 11px !important;
font-weight: 400 !important;
color: var(--muted) !important;
letter-spacing: 0 !important;
text-transform: none !important;
margin-top: 4px !important;
}
/* Inputs */
.controls input[type="text"], .controls input[type="number"],
.controls select, .controls textarea {
background: var(--bg-1) !important;
border: 1px solid var(--line-strong) !important;
border-radius: var(--radius) !important;
color: var(--text) !important;
font-family: var(--num) !important;
font-feature-settings: "tnum", "zero" !important;
padding: 8px 10px !important;
}
.controls input:focus, .controls select:focus, .controls textarea:focus {
outline: none !important;
border-color: var(--focus) !important;
box-shadow: var(--focus-ring) !important;
}
/* Sliders β€” keep the number input box tucked next to the track, not floating right */
.controls input[type="range"] { accent-color: var(--slider-accent) !important; }
.controls .gradio-slider input[type="number"],
.controls .gr-slider input[type="number"],
.controls input.slider-value {
max-width: 64px !important;
min-width: 0 !important;
padding: 6px 8px !important;
font-size: 12px !important;
}
/* Seed is a standalone gr.Number β€” without this it renders a full-width,
tall input. Match the compact slider number boxes (height 22px, 12px). */
.controls .seed-field input[type="number"] {
max-width: 88px !important;
min-width: 0 !important;
height: 22px !important;
padding: 0 10px !important;
font-size: 12px !important;
}
/* Pull the slider label and value tight to the left edge */
.controls .gradio-slider, .controls .gr-slider {
padding: 0 !important;
margin: 0 !important;
}
.controls .gradio-slider > .head, .controls .gradio-slider > div:first-child {
margin: 0 !important;
padding: 0 !important;
}
/* Gradio ships some input wrappers with margin-left:auto which pushes inputs
to the right side of their column. Zero it so inputs align to the left. */
.controls .tab-like-container,
.controls [class*="tab-like-container"],
.controls .input-container,
.controls [class*="input-container"] {
margin-left: 0 !important;
margin-right: 0 !important;
}
/* Dropdown input β€” Gradio v6's actual structure is .wrap > .wrap-inner >
.secondary-wrap > input[role="listbox"]. Put the visible border on .wrap
(the outermost), make every inner layer transparent so it doesn't double-
border. Match using :has() on the input role so this rule only fires on
dropdown wraps, not slider/other wraps inside .controls. */
.controls .wrap:has(input[role="listbox"]),
.controls .wrap:has(input.border-none) {
background: var(--bg-1) !important;
border: 1px solid var(--line-strong) !important;
border-radius: var(--radius) !important;
min-height: 38px !important;
width: 100% !important;
box-sizing: border-box !important;
display: flex !important;
align-items: center !important;
}
.controls .wrap:has(input[role="listbox"]) .wrap-inner,
.controls .wrap:has(input[role="listbox"]) .secondary-wrap,
.controls .wrap:has(input[role="listbox"]) .icon-wrap {
background: transparent !important;
border: 0 !important;
width: 100% !important;
}
.controls .wrap:has(input[role="listbox"]) input,
.controls input.border-none,
.controls input[role="listbox"] {
background: transparent !important;
border: 0 !important;
padding: 8px 10px !important;
color: var(--text) !important;
width: 100% !important;
font-family: var(--font-sans) !important;
}
/* Hide any built-in radio/checkbox glyph SVGs Gradio injects inside pill
labels β€” the styled pill IS the indicator. */
.controls .gradio-radio label > svg,
.controls .gradio-checkboxgroup label > svg,
.controls fieldset label > svg {
display: none !important;
}
/* Dropdown popup β€” only style the visual chrome. Let Gradio's JS handle
position so the popup anchors to the input regardless of scroll. */
.gradio-container ul.options,
.gradio-container ul[role="listbox"] {
max-height: 280px !important;
overflow-y: auto !important;
z-index: 1000 !important;
}
/* Dropdown OPTION LIST (the floating popup) β€” must have an opaque bg or it
appears to "float in the air" against the dark page bg. */
.gradio-container .options,
.gradio-container ul.options,
.gradio-container ul[role="listbox"],
.gradio-container .secondary-wrap ul,
.gradio-container [class*="option-list"],
body .options,
body ul[role="listbox"] {
background: var(--panel-2) !important;
background-color: var(--panel-2) !important;
border: 1px solid var(--line-strong) !important;
border-radius: var(--radius) !important;
box-shadow: 0 10px 40px var(--shadow), 0 2px 0 color-mix(in srgb, var(--green) 20%, transparent) inset !important;
font-family: var(--font-sans) !important;
color: var(--text) !important;
padding: 4px 0 !important;
z-index: 1000 !important;
}
.gradio-container .options li,
.gradio-container ul[role="listbox"] li,
.gradio-container [role="option"],
body .options li,
body ul[role="listbox"] li,
body [role="option"] {
background: transparent !important;
color: var(--text) !important;
font-family: var(--font-sans) !important;
font-size: 13px !important;
font-weight: 400 !important;
letter-spacing: 0 !important;
text-transform: none !important;
padding: 8px 14px !important;
cursor: pointer !important;
border: 0 !important;
}
.gradio-container .options li:hover,
.gradio-container ul[role="listbox"] li:hover,
.gradio-container [role="option"]:hover,
body .options li:hover,
body ul[role="listbox"] li:hover,
body [role="option"]:hover {
background: color-mix(in srgb, var(--green) 12%, transparent) !important;
color: var(--green) !important;
}
.gradio-container [role="option"][aria-selected="true"],
body [role="option"][aria-selected="true"] {
background: color-mix(in srgb, var(--green) 18%, transparent) !important;
color: var(--green) !important;
}
/* Checkbox */
.controls input[type="checkbox"] { accent-color: var(--slider-accent) !important; }
/* Sample preset chip buttons */
.controls button {
background: transparent !important;
border: 1px solid var(--line) !important;
border-radius: var(--radius) !important;
color: var(--text-2) !important;
font-family: var(--font-sans) !important;
font-size: 12px !important;
font-weight: 500 !important;
letter-spacing: 0 !important;
text-transform: none !important;
padding: 8px 14px !important;
transition: border-color 160ms ease, color 160ms ease, background 160ms ease;
}
/* Sample preset chips: only the BORDER tints green on hover. We intentionally
do NOT change text color here, because this rule blanket-matches every
button inside .controls β€” including the primary Generate CTA β€” and tinting
text green there would make it invisible against the green button bg. */
.controls button:not(.primary-cta):hover { border-color: var(--green) !important; background: var(--pill-hover-bg) !important; }
/* ─────────────── UNIFIED SELECTED STATE ─────────────── */
/* Pattern: active = filled accent bg + accent border + accent text.
Hover (non-selected) = outline only. Applied across body region toggles,
anatomy chips, window/level presets β€” one consistent visual pattern. */
/* Radio / Checkbox group containers: zero EVERY potentially-padded wrapper so
the first pill aligns flush with the column's left edge, matching the
slider labels above. Browser default fieldset has ~32px padding-inline-start
plus min-inline-size, and Gradio adds extra wrappers on top of that. */
.controls .gradio-radio,
.controls .gradio-radio > *,
.controls .gradio-radio fieldset,
.controls .gradio-radio fieldset > *,
.controls .gradio-radio .wrap,
.controls .gradio-radio .wrap-inner,
.controls .gradio-checkboxgroup,
.controls .gradio-checkboxgroup > *,
.controls .gradio-checkboxgroup fieldset,
.controls .gradio-checkboxgroup fieldset > *,
.controls .gradio-checkboxgroup .wrap,
.controls .gradio-checkboxgroup .wrap-inner,
.controls fieldset {
padding-left: 0 !important;
padding-inline-start: 0 !important;
margin-left: 0 !important;
margin-inline-start: 0 !important;
border-inline-start-width: 0 !important;
min-inline-size: 0 !important;
}
/* Restore fieldset to a clean flex pill row */
.controls .gradio-radio fieldset,
.controls .gradio-checkboxgroup fieldset {
display: flex !important;
flex-wrap: wrap !important;
gap: 6px !important;
padding: 0 !important;
margin: 0 !important;
border: 0 !important;
min-inline-size: 0 !important;
}
/* Body region (gr.CheckboxGroup) β€” strip the native check, style label as toggle */
.controls .gradio-checkboxgroup label,
.controls fieldset label {
display: inline-flex !important; align-items: center !important;
padding: 8px 14px !important;
border: 1px solid var(--line) !important;
background: transparent !important;
font-family: var(--font-sans) !important;
font-size: 12px !important;
font-weight: 500 !important;
color: var(--text-2) !important;
cursor: pointer;
transition: all 160ms ease;
margin: 0 6px 6px 0 !important;
}
.controls .gradio-checkboxgroup label > input,
.controls fieldset label > input { display: none !important; }
.controls .gradio-checkboxgroup label > span,
.controls fieldset label > span {
font-family: var(--font-sans) !important;
font-size: 12px !important;
font-weight: 500 !important;
color: inherit !important;
letter-spacing: 0 !important;
text-transform: none !important;
margin: 0 !important; /* kill Gradio's ml-2 utility so pill text is centered */
}
.controls .gradio-checkboxgroup label:hover,
.controls fieldset label:hover {
border-color: var(--line-bright) !important;
background: var(--pill-hover-bg) !important;
color: var(--text) !important;
}
.controls .gradio-checkboxgroup label:has(input:checked),
.controls fieldset label:has(input:checked) {
background: color-mix(in srgb, var(--green) 14%, transparent) !important;
border-color: var(--green) !important;
color: var(--green) !important;
}
/* Primary CTA: Generate β€” flat matte NVIDIA green, identical in light + dark.
Gradio puts elem_classes on the <button>, so every state targets
`button.primary-cta` directly; the old descendant form `.primary-cta button`
matched nothing, which is why the hover/active states silently failed. */
button.primary-cta {
margin: 18px 0 6px !important;
padding: 14px 20px !important;
background: #76b900 !important;
color: #0b1a00 !important;
border: 1px solid #5d9100 !important;
border-radius: var(--radius) !important;
font-family: var(--font-sans) !important;
font-size: 14px !important;
font-weight: 600 !important;
letter-spacing: 0 !important;
text-transform: none !important;
position: relative;
width: 100% !important;
cursor: pointer !important;
box-shadow: none !important;
transition: background 140ms ease !important;
}
.controls button.primary-cta:hover,
button.primary-cta:hover {
background: #84cc00 !important;
border-color: #5d9100 !important;
}
button.primary-cta:active {
background: #6aa600 !important;
}
/* Lock the Generate button text colour across normal/hover/active states and
any Svelte-hashed descendant spans. (Disabled overrides this below.) */
button.primary-cta,
button.primary-cta:hover,
button.primary-cta:focus,
button.primary-cta:active,
button.primary-cta *,
button.primary-cta:hover *,
button.primary-cta:active * {
color: #0b1a00 !important;
-webkit-text-fill-color: #0b1a00 !important;
}
/* Disabled / generating state β€” the :disabled:hover pseudo adds specificity so
it beats the plain :hover rule even while the pointer is over the button. */
.controls button.primary-cta:disabled,
button.primary-cta:disabled,
button.primary-cta:disabled:hover {
background: var(--cta-off-a) !important;
color: var(--cta-off-text) !important;
-webkit-text-fill-color: var(--cta-off-text) !important;
cursor: wait !important;
opacity: 1 !important;
box-shadow: none !important;
border-color: var(--cta-off-border) !important;
}
button.primary-cta:disabled *,
button.primary-cta:disabled:hover * {
color: var(--cta-off-text) !important;
-webkit-text-fill-color: var(--cta-off-text) !important;
}
button.primary-cta:disabled::after {
content: "";
display: inline-block;
width: 10px; height: 10px;
margin-left: 10px;
border: 2px solid color-mix(in srgb, currentColor 32%, transparent);
border-top-color: currentColor;
border-radius: 50%;
animation: btn-spin 0.8s linear infinite;
vertical-align: -2px;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
/* ─────────────── viewer panel ─────────────── */
.viewer-col { padding-left: 0 !important; }
.viewer-strip {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 14px;
border: 1px solid var(--line);
border-bottom: none;
border-radius: var(--radius) var(--radius) 0 0;
background: var(--panel);
font-family: var(--font-mono); font-size: 10px;
letter-spacing: 0.20em; text-transform: uppercase;
}
.viewer-strip-left { color: var(--muted); }
.viewer-strip-right { color: var(--text-2); }
.gradio-container .viewer {
background: var(--viewer-bg) !important;
border: 1px solid var(--line) !important;
border-radius: 0 0 var(--radius) var(--radius) !important;
box-shadow: var(--shadow-card) !important;
padding: 0 !important;
position: relative;
}
/* Window/Level preset bar β€” its own self-contained card with breathing room
above and below so it doesn't blend into the viewer / legend stack. */
.gradio-container .preset-row {
padding: 14px 18px !important;
gap: 14px !important;
align-items: center !important;
margin: 14px 0 !important;
border: 1px solid var(--line) !important;
border-radius: var(--radius) !important;
box-shadow: var(--shadow-card) !important;
background:
linear-gradient(180deg, color-mix(in srgb, var(--mr) 10%, transparent) 0%, color-mix(in srgb, var(--mr) 3%, transparent) 100%),
var(--panel) !important;
position: relative;
}
.preset-row::before {
/* Thin left-edge accent to mark this as a control surface */
content: ""; position: absolute; left: 0; top: 0; bottom: 0;
width: 2px;
background: linear-gradient(180deg, color-mix(in srgb, var(--mr) 55%, transparent), color-mix(in srgb, var(--mr) 15%, transparent));
}
.preset-label {
font-family: var(--font-mono) !important;
font-size: 10px;
letter-spacing: 0.20em;
text-transform: uppercase;
color: var(--preset-label);
padding: 0 !important;
}
/* Window/Level preset radio β€” filled-accent active state for consistency */
.preset-row label {
font-family: var(--font-sans) !important;
font-size: 11px !important;
font-weight: 500 !important;
letter-spacing: 0 !important;
text-transform: none !important;
color: var(--text-2) !important;
cursor: pointer;
padding: 7px 12px !important;
border: 1px solid var(--line) !important;
background: transparent !important;
transition: all 160ms ease;
}
.preset-row input[type="radio"] { display: none; }
/* kill Gradio's ml-2 utility so W/L preset text is centered in its pill */
.preset-row label > span { margin: 0 !important; }
/* Gradio's radio/checkbox <fieldset> defaults to overflow:auto, which clips
the first pill's 1px left border where it sits flush at the fieldset edge
(only noticeable in light mode, where the border has real contrast). */
.preset-row fieldset,
.controls fieldset { overflow: visible !important; }
.preset-row label:has(input:checked) {
background: color-mix(in srgb, var(--green) 14%, transparent) !important;
border-color: var(--green) !important;
color: var(--green) !important;
}
.preset-row label:hover { border-color: var(--line-bright) !important; background: var(--pill-hover-bg) !important; color: var(--text) !important; }
/* Status / runtime line β€” labeled metric chips after generation */
.status {
padding: 14px 0 6px !important;
font-family: var(--font-sans) !important;
font-size: 11px !important;
color: var(--text-2) !important;
margin: 0 !important;
border-top: 1px dashed var(--line);
margin-top: 6px !important;
}
/* When status text is replaced with structured HTML chips (post-generation) */
.status .stat-line {
display: flex; flex-wrap: wrap; align-items: center;
gap: 8px 10px;
row-gap: 8px;
}
.status .stat-mark {
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
background: var(--green); box-shadow: 0 0 6px var(--green-glow);
}
.status .stat-label {
font-family: var(--font-sans); font-size: 12px;
color: var(--text); font-weight: 500;
}
.status .stat-chip {
display: inline-flex; align-items: baseline; gap: 6px;
padding: 3px 8px;
border: 1px solid var(--line);
background: var(--chip-bg);
font-family: var(--font-mono); font-size: 10px;
white-space: nowrap;
}
.status .stat-k { color: var(--muted); letter-spacing: 0.12em; text-transform: uppercase; }
.status .stat-v { color: var(--text); font-feature-settings: "tnum", "zero"; }
.status .stat-err { color: var(--warn); }
/* License banner β€” MR */
.license-banner {
margin: 0 0 28px !important;
padding: 14px 18px 16px;
border: 1px solid color-mix(in srgb, var(--warn) 25%, transparent);
border-left: 3px solid var(--warn);
background: var(--warn-soft);
color: var(--warn);
font-family: var(--font-sans);
font-size: 12px;
line-height: 1.5;
border-radius: var(--radius);
}
.license-banner strong {
display: inline-block;
margin-right: 8px;
font-family: var(--font-mono);
letter-spacing: 0.14em;
text-transform: uppercase;
font-size: 10px;
font-weight: 600;
}
.license-banner a { color: var(--license-link); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
/* ─────────────── empty viewport: 2x2 wireframe preview of what's coming ─────────────── */
/* The viewer surface (and this placeholder) stay dark in both themes β€”
radiology/PACS convention; grayscale volumes read most accurately on dark. */
.nv-empty {
position: relative;
width: 100%; aspect-ratio: 1/1; max-height: 720px;
background:
radial-gradient(circle at 50% 50%, rgba(95, 180, 255, 0.06) 0%, transparent 55%),
var(--viewer-bg);
color: #7280a0;
overflow: hidden;
}
/* The 2x2 grid showing where each MPR pane will render after generation */
.nv-empty-wireframe {
position: absolute; inset: 14px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 14px;
}
.nv-wireq {
position: relative;
border: 1px dashed rgba(140, 180, 240, 0.18);
background:
radial-gradient(circle at 50% 50%, rgba(140, 180, 240, 0.025), transparent 70%);
display: flex; align-items: flex-end; justify-content: flex-start;
padding: 10px 12px;
}
.nv-wireq span {
font-family: var(--font-mono); font-size: 9.5px;
letter-spacing: 0.20em; text-transform: uppercase;
color: rgba(140, 180, 240, 0.55);
}
/* Faint crosshair at the center of each pane to suggest the MPR cursor */
.nv-wireq::before, .nv-wireq::after {
content: ""; position: absolute;
background: rgba(140, 180, 240, 0.18);
}
.nv-wireq::before { width: 1px; height: 28px; left: 50%; top: 50%; transform: translate(-50%, -50%); }
.nv-wireq::after { width: 28px; height: 1px; left: 50%; top: 50%; transform: translate(-50%, -50%); }
/* 3D pane gets a different treatment: an isometric box hint */
.nv-wireq-3d::before, .nv-wireq-3d::after { display: none; }
.nv-wireq-3d {
background-image:
linear-gradient(135deg, transparent 48%, rgba(140, 180, 240, 0.22) 49%, rgba(140, 180, 240, 0.22) 51%, transparent 52%),
linear-gradient(45deg, transparent 48%, rgba(140, 180, 240, 0.10) 49%, rgba(140, 180, 240, 0.10) 51%, transparent 52%),
radial-gradient(circle at 50% 50%, rgba(140, 180, 240, 0.04), transparent 70%);
background-size: 22px 22px, 22px 22px, auto;
background-position: center, center, center;
}
.nv-empty-text {
position: absolute; left: 50%; top: 50%;
transform: translate(-50%, -50%);
text-align: center;
padding: 12px 18px;
background: linear-gradient(180deg, rgba(10, 21, 48, 0.92), rgba(10, 21, 48, 0.82));
border: 1px solid rgba(140, 180, 240, 0.12);
backdrop-filter: blur(6px);
z-index: 2;
min-width: 280px;
}
.nv-empty-icon {
font-family: var(--font-sans);
font-size: 13px;
font-weight: 500;
color: #ecf0fa;
margin-bottom: 6px;
}
.nv-empty-msg {
font-family: var(--font-sans);
font-size: 11.5px;
color: #7280a0;
max-width: 280px; text-align: center;
line-height: 1.5;
}
/* ─────────────── anatomy legend ─────────────── */
/* Gradio applies elem_classes to both the outer .block wrapper AND the inner
.prose container β€” so .legend-host appears on two nested elements. Scope
the panel chrome to the outer .block one only and reset the inner copy. */
.legend-host,
.prose.legend-host {
padding: 0 !important;
background: transparent !important;
border: 0 !important;
margin: 0 !important;
max-width: none !important;
}
.block.legend-host {
padding: 16px 18px !important;
background: var(--panel) !important;
border: 1px solid var(--line) !important;
border-radius: var(--radius) !important;
box-shadow: var(--shadow-card) !important;
margin: 0 0 14px !important;
}
.nv-legend { padding: 0; }
.nv-legend-title {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 500;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--green);
margin-bottom: 12px;
display: flex; align-items: center; gap: 10px;
}
.nv-legend-title::before { content: ""; width: 4px; height: 4px; background: var(--green); }
.nv-legend-count {
font-size: 9px;
color: var(--muted);
border: 1px solid var(--line);
padding: 1px 7px;
letter-spacing: 0.10em;
margin-left: 4px;
}
.nv-legend-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.nv-legend-chip {
display: flex; align-items: center; gap: 8px;
padding: 5px 10px;
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
font-family: var(--font-mono); font-size: 10.5px;
color: var(--text-2);
letter-spacing: 0.02em;
text-transform: lowercase;
}
.nv-swatch { width: 9px; height: 9px; border-radius: 0; display: inline-block; }
/* Download file widget β€” self-contained card sitting below the legend with
visible breathing room above it. Solid border matches the legend / preset
pattern instead of dashed (which read as "drop zone" for a finished file). */
.gradio-container .nv-download {
background: var(--panel) !important;
border: 1px solid var(--line) !important;
border-radius: var(--radius) !important;
box-shadow: var(--shadow-card) !important;
margin: 14px 0 0 !important;
padding: 4px !important;
}
.nv-download .gr-file,
.nv-download [class*="file"] {
background: transparent !important;
border: 0 !important;
border-radius: var(--radius) !important;
}
.nv-download button[aria-label*="Remove" i],
.nv-download button[aria-label*="Clear" i],
.nv-download button[title*="Remove" i],
.nv-download button[title*="Clear" i],
.nv-download .delete-btn,
.nv-download .clear-button,
.nv-download .icon-button[aria-label*="x" i] {
display: none !important;
}
/* Override the label heading inside the file widget so it matches our type system */
.nv-download label,
.nv-download [data-testid="file-label"] {
font-family: var(--font-mono) !important;
font-size: 10px !important;
letter-spacing: 0.18em !important;
text-transform: uppercase !important;
color: var(--muted) !important;
padding: 8px 12px !important;
}
/* Hide Gradio footer */
footer { display: none !important; }
/* ─────────────── credits strip (two-line: papers + affiliations) ─────────────── */
.credits-strip {
display: flex; flex-direction: column; align-items: center; gap: 8px;
padding: 28px 0 8px;
margin-top: 8px;
border-top: 1px solid var(--line);
font-family: var(--font-mono); font-size: 9.5px;
letter-spacing: 0.22em; text-transform: uppercase;
color: var(--muted);
animation: fadein 0.6s ease 0.6s both;
}
.credits-line {
display: flex; justify-content: center; flex-wrap: wrap; gap: 12px;
}
.credits-strip .credits-sep { color: var(--muted-2); opacity: 0.6; }
.credits-strip .credits-strong { color: var(--text-2); }
.credits-strip a { color: inherit; text-decoration: none; transition: color 160ms ease; }
.credits-strip a:hover { color: var(--green); }
.credits-papers a.credits-strong { color: var(--text); }
.credits-affil { font-size: 9px; opacity: 0.85; }
/* (Intentionally no position:relative on gradio-app or .gradio-container β€”
Gradio's dropdown popup uses position:absolute calculated relative to the
nearest positioned ancestor, and adding extra positioned wrappers makes the
popup snap to the wrong containing block.) */
/* Subtle scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg-1); }
::-webkit-scrollbar-thumb { background: var(--line-strong); }
::-webkit-scrollbar-thumb:hover { background: var(--line-bright); }
"""
# Adaptive dual-theme: the CSS block defines both a light ("clinical blue") and
# a dark ("MAISI Console") palette under `:root` / `:root.dark`. Gradio's stock
# Base theme toggles the `.dark` class from the visitor's OS / HF Spaces
# preference, so the whole UI follows along β€” no DOM hacks, no force-dark.
# `color-scheme: light dark` keeps native browser chrome (scrollbars, form
# controls) in step with the active theme.
THEME_HEAD = '<meta name="color-scheme" content="light dark">'
def build_app() -> gr.Blocks:
with gr.Blocks(title="NV-Generate") as app:
hero_group, hero_buttons = render_hero()
ct_group, ct_back = workspace_ct.build(spaces_gpu_ct)
mr_group, mr_back = workspace_mr.build(spaces_gpu_mr)
mrb_group, mrb_back = workspace_mr_brain.build(spaces_gpu_mr_brain)
def _show(active: str):
return (
gr.update(visible=(active == "home")),
gr.update(visible=(active == "ct")),
gr.update(visible=(active == "mr")),
gr.update(visible=(active == "mr_brain")),
)
outputs = [hero_group, ct_group, mr_group, mrb_group]
hero_buttons["ct"].click(lambda: _show("ct"), outputs=outputs)
hero_buttons["mr"].click(lambda: _show("mr"), outputs=outputs)
hero_buttons["mr_brain"].click(lambda: _show("mr_brain"), outputs=outputs)
ct_back.click(lambda: _show("home"), outputs=outputs)
mr_back.click(lambda: _show("home"), outputs=outputs)
mrb_back.click(lambda: _show("home"), outputs=outputs)
return app
if __name__ == "__main__":
app = build_app()
app.launch(
server_name="0.0.0.0",
server_port=int(os.environ.get("PORT", "7860")),
show_error=True,
css=CSS,
theme=gr.themes.Base(),
head=THEME_HEAD,
)