| """ |
| ui/theme.py |
| ----------- |
| NOVA's look: a Gradio theme object for the component primitives, plus the CSS |
| that carries the actual identity. |
| |
| Ported from the Streamlit build's utils/styles.py — same palette, same brand |
| header, same speech bubbles, same cluster/paper cards, same live checklist. |
| Gradio hands us a real DOM instead of Streamlit's opaque `data-testid` tree, so |
| everything here hooks `elem_classes`/`elem_id` we set ourselves rather than |
| selectors that break on the next framework release. |
| |
| FORCE_DARK is a js hook, not CSS: Gradio decides light/dark from a URL param |
| before our stylesheet loads, and the palette below only exists in dark. |
| """ |
|
|
| import gradio as gr |
|
|
| NOVA_THEME = gr.themes.Base( |
| primary_hue=gr.themes.Color( |
| c50="#f2efff", c100="#e5dfff", c200="#cabfff", c300="#b09fff", |
| c400="#957fff", c500="#7c5cff", c600="#6344e6", c700="#4a2fbf", |
| c800="#331f99", c900="#1f1273", c950="#0f0847", |
| ), |
| secondary_hue=gr.themes.Color( |
| c50="#ecfeff", c100="#cffafe", c200="#a5f3fc", c300="#67e8f9", |
| c400="#22d3ee", c500="#06b6d4", c600="#0891b2", c700="#0e7490", |
| c800="#155e75", c900="#164e63", c950="#083344", |
| ), |
| neutral_hue=gr.themes.Color( |
| c50="#f2f4fb", c100="#e6e9f2", c200="#cbd0e0", c300="#8b93a7", |
| c400="#5b6377", c500="#3a4157", c600="#252c40", c700="#111828", |
| c800="#0d1322", c900="#080b16", c950="#05070f", |
| ), |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"], |
| ).set( |
| body_background_fill="#080b16", |
| body_background_fill_dark="#080b16", |
| block_background_fill="transparent", |
| block_background_fill_dark="transparent", |
| block_border_width="0px", |
| block_shadow="none", |
| panel_background_fill="transparent", |
| panel_background_fill_dark="transparent", |
| border_color_primary="rgba(255,255,255,0.08)", |
| border_color_primary_dark="rgba(255,255,255,0.08)", |
| input_background_fill="rgba(8,11,22,0.7)", |
| input_background_fill_dark="rgba(8,11,22,0.7)", |
| input_border_color="rgba(255,255,255,0.08)", |
| input_border_color_dark="rgba(255,255,255,0.08)", |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| FORCE_DARK = """ |
| function() { |
| const mark = (el) => el && el.classList.add('dark'); |
| mark(document.documentElement); |
| mark(document.querySelector('gradio-app')); |
| } |
| """ |
|
|
| CSS = """ |
| :root { |
| --nova-bg: #080b16; |
| --nova-card: #111828; |
| --nova-card-2: #0d1322; |
| --nova-border: rgba(255,255,255,0.08); |
| --nova-text: #e6e9f2; |
| --nova-muted: #8b93a7; |
| --nova-accent: #7c5cff; |
| --nova-accent-2: #22d3ee; |
| } |
| |
| /* ---- pin Gradio's own theme variables to NOVA's dark palette ---- |
| Gradio flips these between light and dark via a `.dark` class on its root. |
| In a light-mode browser that class can be absent, and then Gradio resolves |
| --body-text-color to near-black and --background-fill-primary to literal |
| `white` -- unreadable on our dark ground (this was THE light/dark-browser |
| bug). NOVA is deliberately a single dark console, so force the handful of |
| Gradio variables that would otherwise leak light. `!important` on a custom |
| property wins the cascade no matter the class or the injection order, so the |
| look no longer depends on the `.dark` class landing at all. */ |
| :root { |
| --body-text-color: #e6e9f2 !important; |
| --body-text-color-subdued: #8b93a7 !important; |
| --background-fill-primary: #080b16 !important; |
| --background-fill-secondary: #0d1322 !important; |
| --border-color-primary: rgba(255,255,255,0.08) !important; |
| --input-placeholder-color: #8b93a7 !important; |
| --code-background-fill: #0d1322 !important; |
| --table-odd-background-fill: #0d1322 !important; |
| --table-even-background-fill: #111828 !important; |
| --table-border-color: rgba(255,255,255,0.08) !important; |
| } |
| |
| /* ---- page shell: cosmic gradient + a slow drifting aurora ---- */ |
| gradio-app, .gradio-container { |
| background: |
| radial-gradient(1200px 600px at 12% -10%, rgba(124,92,255,0.18), transparent 55%), |
| radial-gradient(1000px 500px at 100% 0%, rgba(34,211,238,0.12), transparent 50%), |
| var(--nova-bg) !important; |
| max-width: 100% !important; |
| color: var(--nova-text); |
| } |
| .gradio-container { padding-top: 1.6rem !important; } |
| |
| /* starfield + aurora live behind everything and never eat clicks */ |
| gradio-app::before { |
| content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 0; |
| background-image: |
| radial-gradient(1.4px 1.4px at 12% 18%, rgba(255,255,255,.55), transparent), |
| radial-gradient(1.2px 1.2px at 72% 12%, rgba(255,255,255,.42), transparent), |
| radial-gradient(1.6px 1.6px at 38% 62%, rgba(255,255,255,.35), transparent), |
| radial-gradient(1.1px 1.1px at 88% 74%, rgba(255,255,255,.40), transparent), |
| radial-gradient(1.3px 1.3px at 24% 88%, rgba(255,255,255,.30), transparent), |
| radial-gradient(1.2px 1.2px at 58% 34%, rgba(255,255,255,.28), transparent); |
| animation: twinkle 7s ease-in-out infinite alternate; |
| } |
| @keyframes twinkle { from { opacity: .35; } to { opacity: .9; } } |
| |
| #nova-root { position: relative; z-index: 1; max-width: 1180px; margin: 0 auto; } |
| |
| /* hide Gradio's own chrome */ |
| footer, .built-with, .show-api { display: none !important; } |
| |
| /* ---- NOVA brand header ---- */ |
| .nova-brand { display:flex; align-items:center; gap:14px; margin-bottom: 2px; } |
| .nova-mark { |
| font-size: 2.1rem; font-weight: 800; letter-spacing: .06em; |
| background: linear-gradient(120deg, #b8a7ff, #7c5cff 45%, #22d3ee); |
| background-size: 200% auto; |
| -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; |
| animation: shimmer 6s linear infinite; |
| } |
| @keyframes shimmer { to { background-position: 200% center; } } |
| .nova-star { font-size: 1.7rem; filter: drop-shadow(0 0 8px rgba(124,92,255,.7)); animation: pulse-star 3s ease-in-out infinite; } |
| @keyframes pulse-star { 50% { filter: drop-shadow(0 0 16px rgba(124,92,255,1)); transform: scale(1.08); } } |
| .nova-sub { color: var(--nova-muted); font-size: .82rem; letter-spacing:.14em; text-transform: uppercase; } |
| .sonic-chip { |
| margin-left:auto; display:flex; align-items:center; gap:8px; |
| background: rgba(124,92,255,0.1); border:1px solid rgba(124,92,255,0.35); |
| color:#cdbcff; padding:6px 14px; border-radius:999px; font-size:.78rem; font-weight:600; |
| } |
| .sonic-dot { width:8px; height:8px; border-radius:50%; background:#22d3ee; box-shadow:0 0 8px #22d3ee; animation: blink 2s ease-in-out infinite; } |
| @keyframes blink { 50% { opacity:.35; } } |
| |
| /* ---- SONIC speech bubble ---- */ |
| .sonic-row { display:flex; gap:14px; align-items:flex-start; margin: 20px 0 8px; } |
| .sonic-avatar { |
| flex:0 0 48px; width:48px; height:48px; border-radius:14px; overflow:hidden; |
| box-shadow: 0 0 22px rgba(124,92,255,.45); |
| } |
| .sonic-avatar img { width:100%; height:100%; object-fit:cover; display:block; } |
| .sonic-bubble { |
| background: var(--nova-card); border:1px solid var(--nova-border); |
| border-radius: 4px 16px 16px 16px; padding: 14px 18px; color: var(--nova-text); |
| font-size: 1.02rem; line-height:1.5; max-width: 760px; |
| animation: bubble-in .32s cubic-bezier(.2,.9,.3,1.2); |
| } |
| @keyframes bubble-in { from { opacity:0; transform: translateY(6px) scale(.98); } } |
| .sonic-name { color:#9d8cff; font-weight:700; font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; margin-bottom:4px; } |
| |
| /* ---- cluster section header ---- */ |
| .cluster-head { display:flex; align-items:center; gap:12px; margin: 26px 0 6px; } |
| .cluster-bar { width:5px; height:30px; border-radius:3px; } |
| .cluster-title { font-size:1.22rem; font-weight:700; color:#f2f4fb; } |
| .cluster-why { color:var(--nova-muted); font-size:.88rem; line-height:1.5; margin: 0 0 6px 17px; max-width: 900px; } |
| |
| /* ---- paper card ---- */ |
| .paper-card { |
| background: linear-gradient(180deg, var(--nova-card), var(--nova-card-2)); |
| border: 1px solid var(--nova-border) !important; border-radius: 16px !important; |
| padding: 16px !important; |
| transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease; |
| } |
| .paper-card:hover { |
| transform: translateY(-3px); border-color: rgba(124,92,255,0.5) !important; |
| box-shadow: 0 12px 30px rgba(0,0,0,0.4); |
| } |
| .paper-title { font-size: 1.02rem; font-weight: 700; color:#f2f4fb; line-height:1.35; margin-bottom:6px; } |
| .paper-meta { color: var(--nova-muted); font-size: .82rem; margin-bottom: 10px; } |
| .src-badge { display:inline-block; padding:2px 9px; border-radius:999px; font-size:.68rem; font-weight:700; |
| margin-right:6px; border:1px solid transparent; } |
| |
| /* ---- buttons ---- */ |
| #nova-root button.secondary, #nova-root a.button { |
| border-radius: 10px !important; font-weight: 600 !important; font-size: .84rem !important; |
| border: 1px solid var(--nova-border) !important; background: rgba(255,255,255,0.04) !important; |
| color: #dfe3ee !important; transition: all .14s ease; |
| } |
| #nova-root button.secondary:hover, #nova-root a.button:hover { |
| border-color: rgba(124,92,255,0.6) !important; background: rgba(124,92,255,0.12) !important; color:#fff !important; |
| } |
| #nova-root button.primary { |
| background: linear-gradient(120deg, #7c5cff, #22d3ee) !important; border: none !important; |
| color: #0a0e1a !important; font-weight: 800 !important; border-radius: 10px !important; |
| box-shadow: 0 6px 18px rgba(124,92,255,.4) !important; transition: filter .14s ease, transform .14s ease; |
| } |
| #nova-root button.primary:hover { filter: brightness(1.08); transform: translateY(-1px); } |
| #nova-root button:disabled { opacity:.4 !important; cursor: not-allowed !important; } |
| |
| /* link-buttons rendered as anchors inside cards */ |
| .card-link { |
| display:inline-flex; align-items:center; justify-content:center; gap:6px; |
| padding:7px 12px; border-radius:10px; font-weight:600; font-size:.8rem; text-decoration:none !important; |
| border:1px solid var(--nova-border); background:rgba(255,255,255,.04); color:#dfe3ee !important; |
| transition: all .14s ease; flex:1; text-align:center; |
| } |
| .card-link:hover { border-color:rgba(124,92,255,.6); background:rgba(124,92,255,.12); color:#fff !important; } |
| .card-link.dead { opacity:.35; pointer-events:none; } |
| .card-links { display:flex; gap:8px; margin-top:2px; } |
| |
| /* ---- inputs ---- */ |
| #nova-root textarea, #nova-root input[type="text"] { |
| background: rgba(8,11,22,0.7) !important; border:1px solid var(--nova-border) !important; |
| color: var(--nova-text) !important; border-radius: 12px !important; font-size:.95rem !important; |
| } |
| #nova-root textarea:focus, #nova-root input[type="text"]:focus { |
| border-color: rgba(124,92,255,.6) !important; box-shadow: 0 0 0 3px rgba(124,92,255,.15) !important; |
| } |
| .field-label { font-weight:700; color:#cbd0e0; margin: 8px 0 2px; font-size:.9rem; } |
| |
| /* ---- welcome hero ---- */ |
| .hero-figure { display:flex; flex-direction:column; align-items:center; justify-content:center; } |
| .hero-figure img { height:52vh; max-height:540px; width:auto; filter: drop-shadow(0 20px 40px rgba(124,92,231,.35)); animation: float 6s ease-in-out infinite; } |
| @keyframes float { 50% { transform: translateY(-12px); } } |
| .hero-name { margin-top:14px; font-weight:800; letter-spacing:.12em; color:#c9c3ff; font-size:.9rem; } |
| .hero-speech { |
| background: var(--nova-card); border:1px solid var(--nova-border); |
| border-radius: 18px 18px 18px 4px; padding: 20px 22px; color: var(--nova-text); |
| font-size: 1.18rem; line-height:1.55; position:relative; margin-bottom: 18px; |
| } |
| .hero-speech .sonic-name { color:#9d8cff; font-weight:700; font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; margin-bottom:8px; } |
| .hero-answer-label { color:var(--nova-muted); font-size:.82rem; text-transform:uppercase; letter-spacing:.1em; margin: 6px 0 8px; } |
| |
| /* ---- live search checklist ---- */ |
| .search-wrap { max-width: 620px; margin: 6px auto 0; } |
| .step-row { display:flex; align-items:center; gap:14px; padding:11px 4px; font-size:1rem; color:var(--nova-muted); border-bottom:1px solid rgba(255,255,255,.05); } |
| .step-ic { width:26px; height:26px; border-radius:50%; flex:0 0 26px; display:flex; align-items:center; justify-content:center; font-size:.8rem; border:2px solid rgba(255,255,255,.14); } |
| .step-row.done { color:#dfe3ee; } |
| .step-row.done .step-ic { background:linear-gradient(120deg,#7c5cff,#22d3ee); border-color:transparent; color:#0a0e1a; font-weight:800; } |
| .step-row.active { color:#fff; } |
| .step-row.active .step-ic { border-color:#7c5cff; border-top-color:transparent; animation: spin .8s linear infinite; } |
| .step-count { margin-left:auto; font-size:.8rem; color:var(--nova-muted); } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| |
| /* ---- boot splash + vectorize loading ---- */ |
| .vec-wrap { max-width: 640px; margin: 10px auto; text-align:center; } |
| .vec-figure img { height:150px; width:auto; filter: drop-shadow(0 10px 26px rgba(124,92,231,.4)); animation: float 6s ease-in-out infinite; } |
| .vec-quote { color:#cbd0e6; font-size:1.12rem; line-height:1.55; font-style:italic; margin:18px auto 6px; max-width:520px; min-height:3em; } |
| .vec-quote .q { color:#9d8cff; font-weight:700; } |
| .vec-phase { color:var(--nova-muted); font-size:.85rem; margin-top:10px; } |
| .vec-bar { height:6px; border-radius:999px; background:rgba(255,255,255,.07); overflow:hidden; margin:16px auto 0; max-width:520px; } |
| .vec-fill { height:100%; border-radius:999px; background:linear-gradient(90deg,#7c5cff,#22d3ee); transition: width .35s ease; } |
| |
| /* ---- per-source status chips ---- */ |
| .src-stat-row { display:flex; flex-wrap:wrap; gap:8px; margin: 2px 0 18px; } |
| .src-stat { font-size:.74rem; font-weight:600; padding:4px 11px; border-radius:999px; border:1px solid transparent; } |
| .src-stat.ok { color:#5eead4; background:rgba(45,212,191,.10); border-color:rgba(45,212,191,.35); } |
| .src-stat.warn { color:#fbbf24; background:rgba(251,191,36,.12); border-color:rgba(251,191,36,.45); } |
| .src-stat.err { color:#f87171; background:rgba(248,113,113,.12); border-color:rgba(248,113,113,.45); } |
| .src-stat.muted { color:#8b93a7; background:rgba(255,255,255,.04); border-color:var(--nova-border); } |
| |
| /* ---- chat ---- */ |
| #nova-chat { background: transparent !important; border: none !important; } |
| #nova-chat .message-wrap { gap: 14px !important; } |
| #nova-chat .message { |
| border-radius: 4px 16px 16px 16px !important; border:1px solid var(--nova-border) !important; |
| background: var(--nova-card) !important; color: var(--nova-text) !important; line-height:1.55 !important; |
| } |
| #nova-chat .message.user { |
| border-radius: 16px 4px 16px 16px !important; |
| background: rgba(124,92,231,.12) !important; border-color: rgba(124,92,231,.3) !important; |
| } |
| #nova-chat .avatar-container img { border-radius: 12px !important; box-shadow: 0 0 14px rgba(124,92,255,.35); } |
| |
| .nova-error { |
| border:1px solid rgba(251,191,36,.45); background:rgba(251,191,36,.10); color:#fbbf24; |
| border-radius:12px; padding:12px 16px; margin:10px 0; font-size:.9rem; |
| } |
| |
| @media (max-width: 860px) { |
| .hero-figure img { height:32vh; } |
| .card-links { flex-wrap: wrap; } |
| } |
| """ |
|
|