"""Fusion Lab design tokens: dark, clarity-first palette (Linear/Observable/Bloomberg, not Palantir-dense) plus a Gradio theme + CSS injection built from the same tokens.""" import gradio as gr BG_VOID = "#09090b" BG_SURFACE = "#18181b" BORDER_HAIRLINE = "#27272a" TEXT_PRIMARY = "#a1a1aa" TEXT_SECONDARY = "#71717a" ACCENT = "#FF5722" SIG_POSITIVE = "#3DDC97" SIG_NEUTRAL = "#d4d4d8" SIG_NEGATIVE = "#FF5722" SIG_NOT_SIGNIFICANT = "#3f3f46" CLASS_COLORS = {0: SIG_NEGATIVE, 1: SIG_NEUTRAL, 2: SIG_POSITIVE} CLASS_NAMES = {0: "Negative", 1: "Neutral", 2: "Positive"} FONT_MONO = 'ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, monospace' FONT_SERIF = '"Merriweather", "Georgia", "Times New Roman", serif' FONT_UI = ( '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif' ) def build_theme() -> gr.themes.Base: return gr.themes.Base( primary_hue=gr.themes.colors.emerald, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], font_mono=[gr.themes.GoogleFont("JetBrains Mono"), FONT_MONO], ).set( body_background_fill=BG_VOID, body_background_fill_dark=BG_VOID, background_fill_primary=BG_SURFACE, background_fill_primary_dark=BG_SURFACE, background_fill_secondary=BG_VOID, background_fill_secondary_dark=BG_VOID, border_color_primary=BORDER_HAIRLINE, border_color_primary_dark=BORDER_HAIRLINE, body_text_color=TEXT_PRIMARY, body_text_color_dark=TEXT_PRIMARY, body_text_color_subdued=TEXT_SECONDARY, body_text_color_subdued_dark=TEXT_SECONDARY, color_accent=ACCENT, color_accent_soft=ACCENT, block_background_fill=BG_SURFACE, block_background_fill_dark=BG_SURFACE, block_border_color=BORDER_HAIRLINE, block_border_color_dark=BORDER_HAIRLINE, block_radius="4px", button_primary_background_fill=ACCENT, button_primary_background_fill_dark=ACCENT, button_primary_text_color=BG_VOID, button_primary_text_color_dark=BG_VOID, input_background_fill=BG_VOID, input_background_fill_dark=BG_VOID, input_border_color=BORDER_HAIRLINE, input_border_color_dark=BORDER_HAIRLINE, ) CUSTOM_CSS = f""" :root {{ --fl-bg-void: {BG_VOID}; --fl-bg-surface: {BG_SURFACE}; --fl-border: {BORDER_HAIRLINE}; --fl-text-primary: {TEXT_PRIMARY}; --fl-text-secondary: {TEXT_SECONDARY}; --fl-accent: {ACCENT}; --fl-positive: {SIG_POSITIVE}; --fl-neutral: {SIG_NEUTRAL}; --fl-negative: {SIG_NEGATIVE}; --fl-not-sig: {SIG_NOT_SIGNIFICANT}; }} .gradio-container {{ max-width: 1320px !important; font-family: {FONT_UI} !important; }} .fl-mono, .fl-mono * {{ font-family: {FONT_MONO} !important; font-variant-numeric: tabular-nums; }} /* Scoped sharp-corner + hairline treatment on our own components — deliberately NOT a blanket wildcard border-radius reset, which fights native Gradio component chrome (dropdowns/sliders/tab pills) instead of composing with it. */ .gr-group, .fl-panel, .fl-stat-block, .fl-badge, .fl-turn, .fl-caveat, .fl-diagram-box {{ border-radius: 0px !important; }} .gr-group, .fl-panel {{ border: 1px solid var(--fl-border) !important; background: var(--fl-bg-surface) !important; }} /* Persistent header ribbon (space/header.py) */ .fl-ribbon {{ display: flex; align-items: center; gap: 28px; padding: 14px 20px; margin-bottom: 4px; border: 1px solid var(--fl-border); background: linear-gradient(180deg, var(--fl-bg-surface) 0%, var(--fl-bg-void) 100%); }} .fl-wordmark {{ font-family: {FONT_UI}; font-weight: 800; font-size: 16px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fl-text-primary); white-space: nowrap; }} .fl-wordmark span {{ color: var(--fl-accent); }} .fl-ribbon-divider {{ width: 1px; align-self: stretch; background: var(--fl-border); }} .fl-chip {{ display: flex; flex-direction: column; gap: 1px; min-width: 108px; }} .fl-chip-label {{ font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--fl-text-secondary); }} .fl-chip-value {{ font-family: {FONT_MONO}; font-variant-numeric: tabular-nums; font-size: 18px; font-weight: 700; color: var(--fl-text-primary); }} .fl-chip-value.fl-accent {{ color: var(--fl-accent); }} .fl-chip-dot {{ display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; }} .fl-stat-block {{ display: flex; flex-direction: column; gap: 4px; padding: 16px 20px; border: 1px solid var(--fl-border); background: var(--fl-bg-surface); }} .fl-stat-label {{ font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--fl-text-secondary); }} .fl-stat-value {{ font-family: {FONT_MONO}; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; font-size: 34px; font-weight: 700; color: var(--fl-text-primary); }} .fl-stat-value.fl-accent {{ color: var(--fl-accent); }} .fl-stat-sub {{ font-size: 12px; color: var(--fl-text-secondary); }} /* Architecture diagram (space/diagram.py) */ .fl-diagram {{ display: flex; align-items: center; gap: 10px; overflow-x: auto; padding: 18px 4px; }} .fl-diagram-box {{ flex: 0 0 auto; border: 1px solid var(--fl-border); background: var(--fl-bg-surface); padding: 10px 14px; text-align: center; }} .fl-diagram-box.fl-diagram-highlight {{ border-color: var(--fl-accent); box-shadow: 0 0 0 1px rgba(255, 87, 34, 0.25); }} .fl-diagram-title {{ font-size: 12px; font-weight: 600; color: var(--fl-text-primary); white-space: nowrap; }} .fl-diagram-dim {{ font-family: {FONT_MONO}; font-variant-numeric: tabular-nums; font-size: 11px; color: var(--fl-accent); margin-top: 2px; }} .fl-diagram-sub {{ font-size: 10px; color: var(--fl-text-secondary); margin-top: 2px; max-width: 120px; }} .fl-diagram-arrow {{ flex: 0 0 auto; color: var(--fl-text-secondary); font-size: 16px; }} .fl-diagram-col {{ display: flex; flex-direction: column; gap: 6px; }} /* Dataframe density + tabular numerals */ .fl-panel table, table.svelte-* {{ font-family: {FONT_UI}; }} div[data-testid="dataframe"] td, div[data-testid="dataframe"] th {{ font-family: {FONT_MONO} !important; font-variant-numeric: tabular-nums; font-size: 12.5px !important; padding: 5px 10px !important; }} .fl-class-badge {{ display: inline-flex; align-items: center; gap: 5px; font-size: 12px; }} .fl-class-dot {{ display: inline-block; width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; }} .fl-badge {{ display: inline-block; padding: 3px 10px; font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; border-radius: 3px; }} .fl-badge-significant {{ background: rgba(255, 87, 34, 0.12); color: var(--fl-accent); border: 1px solid rgba(255, 87, 34, 0.35); }} .fl-badge-not-significant {{ background: rgba(74, 85, 104, 0.2); color: var(--fl-text-secondary); border: 1px solid var(--fl-border); }} .fl-caveat {{ border-left: 3px solid var(--fl-neutral); padding: 8px 14px; background: rgba(201, 162, 39, 0.06); font-size: 13px; color: var(--fl-text-secondary); overflow-wrap: break-word; }} .fl-caveat code {{ overflow-wrap: break-word; word-break: break-word; }} .fl-turn {{ padding: 10px 14px; margin-bottom: 6px; border: 1px solid var(--fl-border); border-left-width: 3px; }} .fl-turn-meta {{ font-size: 11px; color: var(--fl-text-secondary); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }} .fl-turn-text {{ font-size: 14px; color: var(--fl-text-primary); line-height: 1.5; }} .fl-turn-weight {{ font-family: {FONT_MONO}; font-variant-numeric: tabular-nums; color: var(--fl-text-secondary); font-size: 12px; }} .fl-section-header {{ font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--fl-text-secondary); margin: 18px 0 8px 0; padding-bottom: 4px; border-bottom: 1px solid var(--fl-border); }} .fl-split-hero {{ display: flex; flex-direction: row; align-items: stretch; background: var(--fl-bg-void); border: 1px solid var(--fl-border); margin-bottom: 32px; }} .fl-split-left {{ flex: 1; padding: 48px; border-right: 1px solid var(--fl-border); }} .fl-split-right {{ flex: 1; padding: 48px; background: rgba(255, 87, 34, 0.03); /* Subtle audio accent background */ }} .fl-serif-text {{ font-family: {FONT_SERIF} !important; font-size: 24px; line-height: 1.6; color: #f4f4f5; font-style: italic; }} .fl-xray-highlight {{ border-left: 4px solid var(--fl-accent) !important; background: rgba(255, 87, 34, 0.08) !important; }} .fl-xray-highlight .fl-turn-text {{ color: var(--fl-accent) !important; font-family: {FONT_MONO} !important; }} /* --- Evidence Room: behavioral summary strip, timeline, evidence panel (space/workspace.py) --- these classes are emitted by workspace.py/transcript.py but were previously unstyled; restored here. */ .fl-summary-strip {{ display: flex; gap: 1px; background: var(--fl-border); border: 1px solid var(--fl-border); margin-bottom: 10px; flex-wrap: wrap; }} .fl-summary-strip .fl-stat-block {{ flex: 1; border: none; padding: 12px 16px; }} .fl-summary-strip .fl-stat-value {{ font-size: 24px; }} .fl-timeline {{ border: 1px solid var(--fl-border); background: var(--fl-bg-surface); padding: 14px 16px; }} .fl-timeline-row {{ display: flex; align-items: center; gap: 10px; height: 30px; margin-bottom: 2px; }} .fl-timeline-label {{ width: 84px; flex: 0 0 auto; font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--fl-text-secondary); }} .fl-timeline-strip {{ position: relative; flex: 1 1 auto; height: 22px; background: var(--fl-bg-void); border: 1px solid var(--fl-border); }} .fl-timeline-bar {{ position: absolute; bottom: 0; width: 3px; background: var(--fl-accent); cursor: pointer; transition: opacity 0.1s; }} .fl-timeline-bar:hover {{ opacity: 0.7; }} .fl-timeline-bar.fl-timeline-selected {{ background: var(--fl-text-primary); box-shadow: 0 0 0 1px var(--fl-accent); }} .fl-timeline-divider {{ position: absolute; top: -4px; bottom: -4px; width: 1px; background: var(--fl-neutral); }} .fl-timeline-section-label {{ position: absolute; top: -18px; font-size: 10px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--fl-text-secondary); }} .fl-timeline-risk-row {{ position: relative; height: 14px; margin: 4px 0 2px 94px; }} .fl-risk-dot {{ position: absolute; top: 2px; width: 7px; height: 7px; border-radius: 50%; background: var(--fl-negative); cursor: pointer; transform: translateX(-50%); }} .fl-risk-dot.fl-timeline-selected {{ box-shadow: 0 0 0 2px var(--fl-text-primary); }} .fl-evidence-panel {{ border: 1px solid var(--fl-border); background: var(--fl-bg-surface); padding: 14px 16px; min-height: 220px; }} .fl-evidence-empty {{ color: var(--fl-text-secondary); font-size: 13px; }} .fl-evidence-quote {{ margin-bottom: 10px; }} .fl-evidence-bullets {{ margin: 0 0 10px 0; padding-left: 18px; }} .fl-evidence-bullet {{ font-size: 12.5px; }} /* --- Verdict card (space/workspace.py: render_verdict_card) --- */ .fl-verdict-card {{ border: 1px solid var(--fl-border); background: var(--fl-bg-surface); margin-bottom: 10px; overflow: hidden; }} .fl-verdict-banner {{ padding: 12px 18px; font-size: 15px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; }} .fl-verdict-banner-correct {{ background: rgba(61, 220, 151, 0.12); color: var(--fl-positive); border-bottom: 1px solid rgba(61, 220, 151, 0.35); }} .fl-verdict-banner-incorrect {{ background: rgba(255, 87, 34, 0.12); color: var(--fl-accent); border-bottom: 1px solid rgba(255, 87, 34, 0.35); }} .fl-verdict-tiles {{ display: flex; flex-wrap: wrap; }} .fl-verdict-tiles .fl-stat-block {{ flex: 1; border: none; border-right: 1px solid var(--fl-border); padding: 12px 16px; }} .fl-verdict-tiles .fl-stat-block:last-child {{ border-right: none; }} .fl-verdict-tiles .fl-stat-value {{ font-size: 22px; display: flex; align-items: center; gap: 8px; }} /* --- Mobile --- the layout above is built on Gradio's own responsive Row/Column grid (columns already collapse to full width below ~320px on their own), but the raw HTML/CSS blocks below (ribbon, split-hero, panels, timeline) bypass that grid and need explicit narrow-viewport rules. */ @media (max-width: 768px) {{ .fl-ribbon {{ flex-wrap: wrap; gap: 14px; padding: 12px 16px; }} .fl-chip {{ min-width: 88px; }} .fl-split-hero {{ flex-direction: column; }} .fl-split-left {{ padding: 20px; border-right: none; border-bottom: 1px solid var(--fl-border); }} .fl-split-right {{ padding: 20px; }} .fl-serif-text {{ font-size: 19px; }} .fl-panel {{ padding: 16px !important; }} .fl-timeline-label {{ width: 60px; font-size: 10px; }} .fl-timeline-row {{ gap: 6px; }} .fl-timeline-risk-row {{ margin-left: 66px; }} .fl-timeline-section-label {{ font-size: 9px; }} }} @media (max-width: 480px) {{ .fl-wordmark {{ font-size: 13px; }} .fl-chip {{ min-width: 74px; }} .fl-chip-value {{ font-size: 15px; }} .fl-stat-value {{ font-size: 26px; }} }} """