"""Astrometrics OS — the Off-Brand visual layer (badge: Off-Brand). Single source of truth for the custom look, kept OUT of app.py so it stays a removable layer: `from core.theme import THEME, CSS, rule, command_bar`. Every token traces to ../DESIGN.md §1 — no ad-hoc colors. Aesthetic: Futuristic Brutalism / LCARS terminal — Command Orange on Void Black, monospace, square corners, signal-colored panes (orange=active, purple=cognition, yellow=human gate, red=veto, green=pass). Two parts: THEME — a gr.themes.Base that recolors Gradio's OWN component variables (so buttons/inputs/blocks obey the palette, not just our CSS overrides). CSS — the structural chrome CSS does on top can't: the LCARS command bar, ribbon tabs, rule headers, elbow corners, focused-pane borders. """ from __future__ import annotations import gradio as gr # --- canonical palette (DESIGN.md §1) ---------------------------------------- VOID = "#131313" SURFACE = "#1f1f1f" SURFACE_HI = "#2a2a2a" ORANGE = "#ff9c00" ORANGE_SOFT = "#ffc384" PURPLE = "#c2c1ff" BLUE = "#5fafff" YELLOW = "#ffff66" TEXT = "#e2e2e2" OUTLINE = "#a28d79" OUTLINE_DIM = "#544433" GREEN = "#82c88c" RED = "#ffb4ab" _MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" def astrometrics_theme() -> gr.themes.Base: """Recolor Gradio's component variables to the Astrometrics palette. Both the base and *_dark variants are set so the deck looks identical regardless of the browser's light/dark preference (a Space defaults to system). Uses .set() to override CSS variables at the theme level — this is the PROPER way to kill borders, reduce spacing, and control component styling. The raw CSS string handles only things the theme can't express (LCARS rule headers, pill radios, command bar, custom icons, etc.). """ t = gr.themes.Base( primary_hue=gr.themes.colors.orange, secondary_hue=gr.themes.colors.blue, neutral_hue=gr.themes.colors.stone, font=[_MONO], font_mono=[_MONO], radius_size=gr.themes.sizes.radius_none, spacing_size=gr.themes.sizes.spacing_sm, # tighter than default — sections sit closer ) pairs = dict( body_background_fill=VOID, body_background_fill_dark=VOID, body_text_color=TEXT, body_text_color_dark=TEXT, body_text_color_subdued=OUTLINE, body_text_color_subdued_dark=OUTLINE, background_fill_primary=VOID, background_fill_primary_dark=VOID, background_fill_secondary=SURFACE, background_fill_secondary_dark=SURFACE, block_background_fill=SURFACE, block_background_fill_dark=SURFACE, # KILL ALL BLOCK BORDERS via theme variables (the right way) block_border_color=VOID, block_border_color_dark=VOID, block_border_width="0px", block_border_width_dark="0px", block_label_background_fill=VOID, block_label_background_fill_dark=VOID, block_label_text_color=ORANGE, block_label_text_color_dark=ORANGE, block_title_text_color=ORANGE, block_title_text_color_dark=ORANGE, # KILL ALL GENERAL BORDERS border_color_primary=VOID, border_color_primary_dark=VOID, border_color_accent=VOID, border_color_accent_dark=VOID, # BORDERS WIDTH ZERO across the board input_border_width="0px", button_border_width="0px", color_accent_soft=SURFACE_HI, color_accent_soft_dark=SURFACE_HI, input_background_fill=VOID, input_background_fill_dark=VOID, input_border_color=OUTLINE_DIM, input_border_color_dark=OUTLINE_DIM, input_border_color_focus=ORANGE, input_border_color_focus_dark=ORANGE, button_primary_background_fill=ORANGE, button_primary_background_fill_dark=ORANGE, button_primary_text_color=VOID, button_primary_text_color_dark=VOID, button_secondary_background_fill=SURFACE, button_secondary_background_fill_dark=SURFACE, button_secondary_text_color=ORANGE, button_secondary_text_color_dark=ORANGE, button_secondary_border_color=VOID, button_secondary_border_color_dark=VOID, slider_color=ORANGE, slider_color_dark=ORANGE, link_text_color=BLUE, link_text_color_dark=BLUE, # SPACING: tighter section padding via theme variables block_padding="6px", ) return t.set(**pairs) THEME = astrometrics_theme() # --- structural chrome the theme can't express ------------------------------- CSS = f""" /* kill the stock Gradio tells */ footer, .gradio-container footer {{ display:none !important; }} .gradio-container {{ padding-top:4px; }} /* deck frame around the whole console */ #ce-deck {{ border:1px solid var(--ao-outline-dim); border-top:2px solid var(--ao-orange); padding:14px 16px 4px; background:linear-gradient(180deg, rgba(255,156,0,0.03), transparent 120px); }} /* flatten Gradio's boxed labels into bare instrument labels */ .block {{ box-shadow:none !important; }} .block > .label-wrap, span[data-testid="block-info"] {{ background:transparent !important; border:none !important; padding:0 0 2px !important; }} .block .label-wrap span, label > span:first-child {{ background:transparent !important; }} /* a "console" column: NO side border (was: border-left:2px solid var(--ao-orange)) */ .ce-console {{ padding-left:4px !important; padding-right:4px !important; gap:16px !important; }} """ CSS += f""" :root, .gradio-container {{ --ao-void:{VOID}; --ao-surface:{SURFACE}; --ao-surface-hi:{SURFACE_HI}; --ao-orange:{ORANGE}; --ao-orange-soft:{ORANGE_SOFT}; --ao-purple:{PURPLE}; --ao-blue:{BLUE}; --ao-yellow:{YELLOW}; --ao-text:{TEXT}; --ao-outline:{OUTLINE}; --ao-outline-dim:{OUTLINE_DIM}; --ao-green:{GREEN}; --ao-red:{RED}; }} .gradio-container {{ background:var(--ao-void) !important; max-width:1600px !important; }} * {{ font-family:{_MONO} !important; }} .gradio-container .prose, .gradio-container p, .gradio-container span, .gradio-container li {{ color:var(--ao-text); }} /* ── LCARS COMMAND BAR ─────────────────────────────────────────────── */ #ce-cmdbar {{ display:flex; align-items:stretch; gap:0; margin-bottom:14px; }} #ce-cmdbar .ce-elbow {{ background:var(--ao-orange); color:var(--ao-void); font-weight:800; letter-spacing:3px; text-transform:uppercase; font-size:15px; padding:10px 22px 10px 16px; border-top-right-radius:18px; white-space:nowrap; display:flex; align-items:center; }} #ce-cmdbar .ce-bar {{ flex:1; background:var(--ao-surface-hi); border-bottom:2px solid var(--ao-orange); display:flex; align-items:center; justify-content:flex-end; gap:18px; padding:0 16px; font-size:11px; letter-spacing:1px; text-transform:uppercase; }} #ce-cmdbar .ce-bar b {{ color:var(--ao-orange); }} #ce-cmdbar #ce-clock {{ color:var(--ao-blue); }} .ce-sub {{ color:var(--ao-outline); letter-spacing:.5px; font-size:12px; line-height:1.5; }} .ce-sub b {{ color:var(--ao-orange-soft); }} /* ── RULE HEADERS ── LABEL ──────────── (flex line fills the width) */ .ce-rule {{ display:flex; align-items:center; gap:10px; color:var(--ao-orange); text-transform:uppercase; letter-spacing:3px; font-size:12px; font-weight:700; margin:10px 0 4px; }} .ce-rule::after {{ content:""; flex:1; height:0; border-top:1px solid var(--ao-outline-dim); }} .ce-rule::before {{ content:"▸"; color:var(--ao-orange); }} /* labels as orange uppercase */ .block .label-wrap span, label > span, span[data-testid="block-info"] {{ color:var(--ao-orange) !important; text-transform:uppercase; letter-spacing:1.4px; font-size:11px !important; font-weight:700; }} /* square brutalist blocks, dim borders removed for clean look */ .block, .form, .gr-box, .gr-panel {{ border-radius:0 !important; border:none !important; background:transparent !important; }} textarea, input, select, .gr-input {{ background:var(--ao-void) !important; color:var(--ao-text) !important; border-radius:0 !important; }} textarea:focus, input:focus {{ border-color:var(--ao-orange) !important; box-shadow:none !important; }} /* ── RIBBON TABS (LCARS elbows) ── each tab a rounded ribbon; selected = solid orange */ .tab-nav, .tab-container {{ border-bottom:2px solid var(--ao-orange) !important; gap:6px !important; padding-bottom:0 !important; }} .tab-nav button, .tab-container button, button[role="tab"] {{ color:var(--ao-orange-soft) !important; background:var(--ao-outline-dim) !important; text-transform:uppercase; letter-spacing:2px; font-size:12px !important; font-weight:800; border:none !important; border-radius:14px 14px 0 0 !important; padding:8px 22px !important; margin:0 !important; }} .tab-nav button:hover, button[role="tab"]:hover {{ color:var(--ao-void) !important; background:var(--ao-orange-soft) !important; }} .tab-nav button.selected, button[role="tab"][aria-selected="true"], .tab-container button.selected {{ background:var(--ao-orange) !important; color:#131313 !important; }} /* ── file dropzone as a clean drop area (no border) ── */ .ce-drop, .ce-drop .center, .ce-drop [data-testid="block-label"] {{ background:var(--ao-void) !important; }} .ce-drop {{ border:none !important; border-radius:0 !important; }} .ce-drop:hover {{ border-color:var(--ao-orange) !important; }} .ce-drop .wrap, .ce-drop .or, .ce-drop button {{ color:var(--ao-orange-soft) !important; text-transform:uppercase; letter-spacing:1px; font-size:12px; }} /* ── ICON BUTTONS (SVG via CSS mask, no raw HTML in labels) ── */ /* Each icon class injects a 12x12 SVG via mask-image. The SVG is a data URL so no external requests. Color inherits from currentColor. */ .ce-pillbtn {{ display:inline-flex !important; align-items:center !important; gap:6px !important; }} .ce-icon-bolt::before, .ce-icon-shuffle::before, .ce-icon-anchor::before, .ce-icon-refresh::before, .ce-icon-play::before, .ce-icon-printer::before, .ce-icon-search::before, .ce-icon-check::before, .ce-icon-x::before, .ce-icon-arrow::before, .ce-icon-target::before, .ce-icon-layers::before, .ce-icon-flask::before, .ce-icon-gauge::before, .ce-icon-info::before, .ce-icon-sliders::before {{ content:""; display:inline-block; width:12px; height:12px; flex:0 0 12px; background:currentColor; -webkit-mask-size:contain; mask-size:contain; -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat; -webkit-mask-position:center; mask-position:center; }} .ce-icon-bolt::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,"); mask-image:url("data:image/svg+xml;utf8,"); }} .ce-icon-shuffle::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,"); mask-image:url("data:image/svg+xml;utf8,"); }} .ce-icon-anchor::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,"); mask-image:url("data:image/svg+xml;utf8,"); }} .ce-icon-refresh::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,"); mask-image:url("data:image/svg+xml;utf8,"); }} .ce-icon-play::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,"); mask-image:url("data:image/svg+xml;utf8,"); }} .ce-icon-printer::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,"); mask-image:url("data:image/svg+xml;utf8,"); }} .ce-icon-info::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,"); mask-image:url("data:image/svg+xml;utf8,"); }} .ce-icon-sliders::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,"); mask-image:url("data:image/svg+xml;utf8,"); }} .ce-icon-arrow::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,"); mask-image:url("data:image/svg+xml;utf8,"); }} /* Arrow AFTER text (right-side icon — indicates "next step"). For buttons whose label means "proceed to next stage" (SLICE →, PRINT →, REFRESH →). Gradio wraps each button in a .wrap; elem_classes lands on that wrapper, so we target the label inside the