""" 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)", ) # Put Gradio into its dark mode WITHOUT reloading the page. # # The previous version redirected to ?__theme=dark. That's a page reload racing # SSR: sometimes the `.dark` class landed (dark, correct), sometimes it didn't # (light — near-black text on our dark ground, invisible). Two devices, two race # outcomes, one "why does it look different?" bug. Adding the class directly on # load is deterministic and flash-free. The CSS below is the real backstop, so # even if this never ran the page would still be readable. 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; } } """