"""Visual identity for the QC operator console. DESIGN INTENT. This is instrument software, not a consumer app. The reference points are laboratory and process-control interfaces: dark neutral ground, high-contrast data, monospace for anything a reader might transcribe (cell IDs, cycle counts, costs), and a clean sans-serif for prose. THE COLOUR RULE, which is load-bearing rather than aesthetic. Green, amber and red carry DECISION meaning -- accept, continue, reject -- and are used for nothing else anywhere in the interface. A chart axis, a section header or a hover state never uses them. An operator must be able to glance at a panel and read its colour as a decision without checking what the colour refers to. Data series therefore use the neutral/blue/violet ramp, and the one exception is the campaign-shift alarm state, which is deliberately red because it IS a decision: do not trust this result. """ from __future__ import annotations import gradio as gr # --- ground and structure --------------------------------------------------- BG = "#0d1117" # page PANEL = "#161b22" # cards PANEL_RAISED = "#1c2430" # inputs, table headers BORDER = "#30363d" BORDER_STRONG = "#484f58" # --- type ------------------------------------------------------------------- TEXT = "#e6edf3" TEXT_MUTED = "#8b949e" TEXT_DIM = "#6e7681" # --- data (never used for decisions) ---------------------------------------- DATA = "#58a6ff" # primary series DATA_ALT = "#a371f7" # secondary series DATA_BAND = "#1f6feb" # uncertainty bands GRID = "#21262d" # --- decision semantics (used for NOTHING else) ----------------------------- ACCEPT = "#3fb950" CONTINUE = "#d29922" REJECT = "#f85149" ALARM = "#f85149" DECISION_COLOURS = {"A": ACCEPT, "B": CONTINUE, "C": REJECT, "CONTINUE": CONTINUE} MONO = "'JetBrains Mono', 'SF Mono', 'Cascadia Mono', Consolas, monospace" SANS = "'Inter', -apple-system, 'Segoe UI', Roboto, sans-serif" def build_theme() -> gr.Theme: """A restrained technical theme, not the Gradio default.""" return gr.themes.Base( primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.slate, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "Consolas", "monospace"], ).set( body_background_fill=BG, body_text_color=TEXT, background_fill_primary=PANEL, background_fill_secondary=BG, block_background_fill=PANEL, block_border_color=BORDER, block_border_width="1px", block_label_background_fill=PANEL, block_label_text_color=TEXT_MUTED, block_title_text_color=TEXT, border_color_primary=BORDER, input_background_fill=PANEL_RAISED, input_border_color=BORDER, button_primary_background_fill=DATA_BAND, button_primary_text_color="#ffffff", button_secondary_background_fill=PANEL_RAISED, button_secondary_text_color=TEXT, panel_background_fill=PANEL, table_odd_background_fill=PANEL, table_even_background_fill=PANEL_RAISED, ) CSS = f""" :root {{ --ct-bg: {BG}; --ct-panel: {PANEL}; --ct-raised: {PANEL_RAISED}; --ct-border: {BORDER}; --ct-text: {TEXT}; --ct-muted: {TEXT_MUTED}; --ct-accept: {ACCEPT}; --ct-continue: {CONTINUE}; --ct-reject: {REJECT}; --ct-data: {DATA}; }} .gradio-container {{ max-width: 1400px !important; }} /* ---------- masthead ---------- */ .ct-masthead {{ border-bottom: 1px solid var(--ct-border); padding: 0 0 14px 0; margin-bottom: 6px; }} .ct-masthead h1 {{ font-family: {MONO}; font-size: 1.45rem; font-weight: 600; letter-spacing: 0.02em; color: var(--ct-text); margin: 0 0 4px 0; }} .ct-masthead .ct-sub {{ color: var(--ct-muted); font-size: 0.86rem; margin: 0; }} .ct-masthead .ct-build {{ font-family: {MONO}; font-size: 0.72rem; color: {TEXT_DIM}; margin-top: 6px; }} /* ---------- decision badge ---------- */ .ct-decision {{ border-radius: 6px; padding: 18px 22px; margin-bottom: 10px; border-left: 5px solid var(--ct-border); background: var(--ct-raised); }} .ct-decision .ct-label {{ font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ct-muted); margin-bottom: 6px; }} .ct-decision .ct-verdict {{ font-family: {MONO}; font-size: 1.9rem; font-weight: 700; line-height: 1.1; }} .ct-decision .ct-tier {{ color: var(--ct-muted); font-size: 0.9rem; margin-top: 4px; }} .ct-accept {{ border-left-color: var(--ct-accept); }} .ct-accept .ct-verdict {{ color: var(--ct-accept); }} .ct-continue {{ border-left-color: var(--ct-continue); }} .ct-continue .ct-verdict {{ color: var(--ct-continue); }} .ct-reject {{ border-left-color: var(--ct-reject); }} .ct-reject .ct-verdict {{ color: var(--ct-reject); }} /* ---------- readouts ---------- */ .ct-readout {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1px; background: var(--ct-border); border: 1px solid var(--ct-border); border-radius: 6px; overflow: hidden; margin-bottom: 10px; }} .ct-readout .ct-cell {{ background: var(--ct-panel); padding: 12px 14px; }} .ct-readout .ct-k {{ font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ct-muted); margin-bottom: 5px; }} .ct-readout .ct-v {{ font-family: {MONO}; font-size: 1.15rem; color: var(--ct-text); font-weight: 600; }} .ct-readout .ct-u {{ font-size: 0.72rem; color: {TEXT_DIM}; margin-left: 3px; }} /* ---------- campaign-shift alarm: a state change, not a footnote ---------- */ .ct-alarm {{ border: 1px solid var(--ct-reject); border-left: 5px solid var(--ct-reject); background: rgba(248, 81, 73, 0.09); border-radius: 6px; padding: 16px 20px; margin-bottom: 12px; }} .ct-alarm .ct-alarm-title {{ font-family: {MONO}; font-weight: 700; color: var(--ct-reject); font-size: 0.95rem; letter-spacing: 0.04em; margin-bottom: 8px; }} .ct-alarm p {{ color: var(--ct-text); font-size: 0.85rem; margin: 4px 0; line-height: 1.5; }} .ct-alarm code {{ font-family: {MONO}; color: var(--ct-reject); }} .ct-nominal {{ border: 1px solid var(--ct-border); border-left: 5px solid var(--ct-accept); background: rgba(63, 185, 80, 0.06); border-radius: 6px; padding: 12px 18px; margin-bottom: 12px; }} .ct-nominal .ct-alarm-title {{ font-family: {MONO}; font-weight: 600; color: var(--ct-accept); font-size: 0.85rem; }} .ct-nominal p {{ color: var(--ct-muted); font-size: 0.8rem; margin: 4px 0; }} /* ---------- takeaway strip under each figure ---------- */ .ct-takeaway {{ border-left: 3px solid var(--ct-data); background: var(--ct-raised); padding: 10px 14px; margin: 4px 0 16px 0; border-radius: 0 4px 4px 0; color: var(--ct-text); font-size: 0.84rem; line-height: 1.5; }} .ct-takeaway strong {{ color: var(--ct-data); }} /* ---------- limitations ---------- */ .ct-limit {{ border: 1px solid var(--ct-border); border-radius: 6px; padding: 16px 20px; background: var(--ct-panel); margin-bottom: 10px; }} .ct-limit h3 {{ font-family: {MONO}; font-size: 0.9rem; color: var(--ct-text); margin: 0 0 10px 0; letter-spacing: 0.03em; }} .ct-limit ul {{ margin: 0; padding-left: 18px; }} .ct-limit li {{ color: var(--ct-muted); font-size: 0.84rem; margin-bottom: 7px; line-height: 1.5; }} .ct-limit li strong {{ color: var(--ct-text); }} /* ---------- figure provenance line ---------- */ .ct-source {{ font-family: {MONO}; font-size: 0.7rem; color: {TEXT_DIM}; margin: -8px 0 22px 3px; letter-spacing: 0.02em; }} .ct-mono {{ font-family: {MONO}; }} footer {{ display: none !important; }} """ def matplotlib_style() -> dict: """rcParams matching the console, so figures look native rather than pasted in.""" return { "figure.facecolor": PANEL, "axes.facecolor": PANEL, "savefig.facecolor": PANEL, "text.color": TEXT, "axes.labelcolor": TEXT_MUTED, "axes.edgecolor": BORDER_STRONG, "xtick.color": TEXT_MUTED, "ytick.color": TEXT_MUTED, "grid.color": GRID, "axes.grid": True, "grid.alpha": 0.6, "axes.titlecolor": TEXT, "font.size": 9, "axes.titlesize": 10, "legend.frameon": False, "legend.labelcolor": TEXT_MUTED, "figure.dpi": 110, }