Spaces:
Running on Zero
Running on Zero
| """ | |
| 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, | |
| ) | |