"""Gradio theme and CSS built from the Bit design system. The design system ships six global stylesheets, listed in its own manifest as `globalCssPaths`, and they are loaded here **in that order**: tokens/fonts.css tokens/colors.css tokens/typography.css tokens/spacing.css base.css styles.css `base.css` is the important one and was previously vendored but never loaded. It carries the design's global reset: square corners everywhere (`border-radius:0 !important`), zeroed body margins, heading defaults, link and focus treatment, the 8px pixel-font rule, and the scrollbar styling. Without it the app inherited Gradio's rounded, roomy defaults and only *approximated* the design. Three things fight the design if left alone, and all three are handled here: 1. **Gradio's spacing scale.** Its defaults are `layout_gap: *spacing_xxl` and `block_padding: *spacing_xl` — far looser than the design, which uses 12px panel padding and 6–10px gaps. These are configured on the theme rather than fought selector-by-selector. 2. **Gradio's type scale.** Its `body_text_size` is `*text_md` (16px). The design's workhorse size is `--text-xs` (10px) with `--text-2xs` (8px) for micro labels; `--text-base` (12px) appears exactly once in the whole design. 3. **Rounded corners.** Gradio rounds everything; the design is square. """ from __future__ import annotations from pathlib import Path import gradio as gr ASSETS = Path(__file__).resolve().parent.parent.parent / "assets" FONT_DIR = ASSETS / "fonts" TOKENS = ASSETS / "tokens" # The design system's own manifest order. styles.css is only @import glue for # the others, so it is resolved here rather than fetched. GLOBAL_CSS = ("colors.css", "typography.css", "spacing.css", "base.css") def _font_face_css() -> str: """@font-face rules pointing at Gradio's static file route.""" faces = [ ("Styrene A", "StyreneA-Thin.otf", 100, "normal"), ("Styrene A", "StyreneA-Light.otf", 300, "normal"), ("Styrene A", "StyreneA-Regular.otf", 400, "normal"), ("Styrene A", "StyreneA-Medium.otf", 500, "normal"), ("Mac Minecraft", "MacMinecraft.ttf", 400, "normal"), ] out = [] for family, filename, weight, style in faces: path = FONT_DIR / filename if not path.exists(): continue fmt = "opentype" if filename.endswith(".otf") else "truetype" out.append( f"@font-face{{font-family:'{family}';" f"src:url('/gradio_api/file={path}') format('{fmt}');" f"font-weight:{weight};font-style:{style};font-display:swap}}" ) return "\n".join(out) def _global_css() -> str: """Inline the design system's global stylesheets, verbatim and in order.""" parts = [] for name in GLOBAL_CSS: p = TOKENS / name if p.exists(): parts.append(f"/* ---- design system: {name} ---- */\n{p.read_text()}") return "\n".join(parts) # base.css sets `user-select:none` on everything and re-enables it only for # inputs, code and `.bit-selectable`. That suits a trading dashboard but not a # data tool: people need to select numbers out of the tables. This is the one # deliberate departure from base.css, scoped to data surfaces only. SELECTION_FIX = """ table, table *, .bit-table, .bit-table *, .prose, .prose *, .bit-stat-value, .bit-stat-sub, .bit-sig-row, .bit-sig-row *, .bit-podium, .bit-podium *, .bit-note, .bit-note *, .markdown, .markdown * { user-select: text; } """ # -------------------------------------------------------------------------- # Gradio neutralisation: strip its layout spacing back to the design's scale. # -------------------------------------------------------------------------- GRADIO_RESET = """ /* Gradio wraps every component in padded, gapped containers. Collapse them so the design's own spacing is the only spacing on the page. */ .gradio-container{ max-width:100% !important; padding:0 !important; } .gradio-container .block, .gradio-container .form, .gradio-container .styler, .gradio-container .container, .gradio-container .panel{ background:transparent !important; border:none !important; box-shadow:none !important; padding:0 !important; } .gradio-container .gap{ gap:8px !important; } .gradio-container .form > *{ margin:0 !important; } .gradio-container .block > .wrap{ padding:0 !important; } .gradio-container .tabitem{ padding:10px 0 0 !important; border:none !important; } .gradio-container .column, .gradio-container .row{ gap:8px !important; } .gradio-container .hide-container{ margin:0 !important; padding:0 !important; } .gradio-container span.svelte-1gfkn6j, .gradio-container .block-title{ margin:0 !important; } .gradio-container .wrap.svelte-1sk0pyu{ padding:0 !important; } .gradio-container .icon-button-wrapper{ display:none !important; } .gradio-container .block > label{ margin-bottom:2px !important; } .gradio-container .prose :is(h1,h2,h3,h4){ margin:6px 0 4px !important; } .gradio-container .prose p, .gradio-container .prose li{ margin:2px 0 !important; font-size:var(--text-xs) !important; } .gradio-container .prose ul{ margin:2px 0 !important; padding-left:16px !important; } footer, .gradio-container footer{ display:none !important; } """ SHELL_CSS = """ /* ================= tabs ================= Every tab, header nav and Gradio alike, is Styrene 10px. Gradio 5 renders its tab strip as `.tab-container`, not the `.tab-nav` an older selector assumed, which is why these were still falling back to the system font. */ .gradio-container .tab-container button, .gradio-container .tab-nav > button, .bit-topbar nav button, header nav button{ font-family:var(--font-styrene) !important; font-size:10px !important; text-transform:uppercase; letter-spacing:var(--tracking-wide); } /* ================= shell ================= */ .gradio-container{ background:var(--bg-canvas) !important; font-family:var(--font-body); font-weight:var(--weight-body); color:var(--text-primary); font-size:var(--text-xs); line-height:var(--leading-normal); } /* ================= typography ================= */ .bit-h1,.bit-h2,.bit-h3{ font-family:var(--font-heading); font-weight:var(--weight-heading); letter-spacing:var(--tracking-wide); text-transform:uppercase; color:var(--text-primary); margin:0; } .bit-h1{ font-size:var(--text-md); } .bit-h2{ font-size:var(--text-md); } .bit-h3{ font-size:var(--text-sm); } .bit-micro{ font-family:var(--font-tiny); font-size:var(--text-2xs); letter-spacing:var(--tracking-wider); text-transform:uppercase; color:var(--text-tertiary); line-height:1.7; } .bit-spacer{ flex:1; } /* ================= top bar ================= */ .bit-topbar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; background:var(--bg-panel); border-bottom:1px solid var(--border-default); padding:8px 12px; position:sticky; top:0; z-index:var(--z-header); } .bit-mark{ width:18px; height:18px; flex:0 0 18px; display:inline-block; color:var(--accent-amber); /* the SVG paths use currentColor */ } .bit-wordmark{ font-family:var(--font-heading); font-size:var(--text-md); font-weight:500; letter-spacing:var(--tracking-wide); color:var(--text-primary); } .bit-slash{ color:var(--text-tertiary); } .bit-chip{ display:inline-flex; align-items:center; gap:6px; white-space:nowrap; font-family:var(--font-mono); font-size:var(--text-2xs); border:1px solid var(--border-default); padding:3px 8px; color:var(--text-secondary); background:transparent; } .bit-chip-ok{ color:var(--accent-moss-strong); border-color:var(--accent-moss-dim); } .bit-chip-run{ color:var(--accent-amber-strong); border-color:var(--accent-amber-dim); } .bit-chip-warn{ color:var(--fin-down); border-color:var(--fin-down); } .bit-chip-accent{ color:var(--stone-950); background:var(--accent-amber); border-color:var(--accent-amber); } .bit-chip-ctx{ color:var(--text-tertiary); } a.bit-link{ text-decoration:none; color:var(--text-secondary); } a.bit-link:hover{ background:var(--accent-amber); color:var(--stone-950); border-color:var(--accent-amber); text-decoration:none; } /* ================= panels ================= */ .bit-panel{ background:var(--bg-panel); border:1px solid var(--border-default); padding:12px; margin-bottom:8px; } .bit-panel-head{ display:flex; align-items:baseline; gap:10px; border-bottom:1px solid var(--border-subtle); padding-bottom:6px; margin-bottom:8px; } .bit-zone-title{ font-family:var(--font-heading); text-transform:uppercase; letter-spacing:var(--tracking-wide); font-size:var(--text-md); color:var(--text-primary); padding:10px 2px 6px; display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; } .bit-zone-title .bit-micro{ margin-left:auto; } /* ================= stat band ================= */ .bit-statband{ display:flex; flex-wrap:wrap; border:1px solid var(--border-default); background:var(--bg-panel); margin-bottom:8px; } .bit-stat{ flex:1 1 124px; padding:8px 12px; border-right:1px solid var(--border-subtle); border-bottom:1px solid var(--border-subtle); } .bit-stat-label{ font-family:var(--font-tiny); font-size:var(--text-2xs); letter-spacing:var(--tracking-wider); text-transform:uppercase; color:var(--text-tertiary); } .bit-stat-value{ font-family:var(--font-mono); font-size:var(--text-lg); line-height:var(--leading-tight); color:var(--text-primary); margin:2px 0 1px; letter-spacing:var(--tracking-tight); font-variant-numeric:tabular-nums; } .bit-stat-sub{ font-family:var(--font-mono); font-size:var(--text-2xs); color:var(--text-tertiary); } .bit-up{ color:var(--fin-up-strong); } .bit-down{ color:var(--fin-down-strong); } /* ================= notes ================= */ .bit-note{ font-family:var(--font-mono); font-size:var(--text-2xs); line-height:1.6; border-left:2px solid var(--accent-amber); background:var(--bg-raised); padding:6px 10px; color:var(--text-secondary); margin-bottom:6px; } .bit-note-danger{ border-left-color:var(--fin-down); } .bit-tag{ font-family:var(--font-tiny); font-size:var(--text-2xs); letter-spacing:var(--tracking-wider); border:1px solid var(--border-default); padding:1px 5px; color:var(--text-tertiary); margin-left:4px; } /* ================= empty state ================= */ .bit-empty{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; padding:60px 20px; text-align:center; border:1px solid var(--border-default); background:var(--bg-panel); } .bit-empty-glyph{ font-size:30px; color:var(--accent-amber-dim); line-height:1; } .bit-empty-copy{ color:var(--text-secondary); max-width:46ch; font-size:var(--text-xs); } .bit-kbd-row{ display:flex; gap:6px; flex-wrap:wrap; justify-content:center; } .bit-kbd{ font-family:var(--font-tiny); font-size:var(--text-2xs); letter-spacing:var(--tracking-wider); color:var(--text-tertiary); border:1px solid var(--border-default); padding:3px 7px; } /* ================= run manager ================= */ .bit-run-card{ border:1px solid var(--border-subtle); padding:6px 8px; margin-bottom:4px; background:var(--bg-panel); } .bit-run-card-sel{ border-color:var(--accent-amber-dim); } .bit-run-top{ display:flex; align-items:center; gap:6px; } .bit-run-name{ font-size:var(--text-xs); color:var(--text-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .bit-run-mark{ margin-left:auto; color:var(--accent-amber-strong); font-size:var(--text-2xs); } .bit-run-sharpe{ font-family:var(--font-mono); font-size:var(--text-xs); display:flex; gap:6px; } .bit-run-ret{ margin-left:auto; color:var(--text-tertiary); } .bit-gloss{ margin-bottom:6px; } .bit-stage{ font-size:var(--text-xs); font-family:var(--font-mono); line-height:1.7; } .bit-gloss-def{ font-size:var(--text-2xs); color:var(--text-secondary); line-height:1.6; } /* ================= KPI row ================= */ .bit-kpi-row{ display:flex; flex-wrap:wrap; gap:12px; } .bit-kpi{ min-width:72px; } .bit-kpi-value{ font-family:var(--font-mono); font-size:var(--text-md); line-height:1.1; color:var(--text-primary); } /* ================= podium ================= */ .bit-podium-row{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; } .bit-podium{ flex:1 1 210px; border:1px solid var(--border-default); background:var(--bg-panel); padding:10px 12px; border-top:2px solid var(--border-strong); } .bit-podium-1{ border-top-color:var(--accent-amber); } .bit-podium-2{ border-top-color:var(--accent-moss-strong); } .bit-podium-3{ border-top-color:var(--mute-teal); } .bit-podium-rank{ font-family:var(--font-tiny); font-size:var(--text-2xs); color:var(--text-tertiary); letter-spacing:var(--tracking-wider); } .bit-podium-name{ font-size:var(--text-md); color:var(--text-primary); font-family:var(--font-heading); letter-spacing:var(--tracking-tight); text-transform:none; } .bit-podium-value{ font-family:var(--font-mono); font-size:var(--text-xl); line-height:1.1; margin-top:6px; display:flex; align-items:baseline; gap:6px; font-variant-numeric:tabular-nums; } .bit-podium-unit{ font-family:var(--font-tiny); font-size:var(--text-2xs); letter-spacing:var(--tracking-wider); color:var(--text-tertiary); } /* ================= signal aggregator ================= */ .bit-sig-grid{ display:flex; gap:12px; flex-wrap:wrap; align-items:flex-start; } .bit-sig-list{ flex:1 1 330px; min-width:0; } .bit-sig-row{ display:flex; align-items:center; gap:10px; border:1px solid var(--border-subtle); padding:5px 10px; margin-bottom:3px; font-family:var(--font-mono); font-size:var(--text-xs); } .bit-sig-name{ flex:1 1 auto; color:var(--text-secondary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .bit-sig-dir{ width:70px; text-align:right; } .bit-sig-edge{ width:60px; text-align:right; } .bit-sig-acc,.bit-sig-w{ width:58px; text-align:right; color:var(--text-tertiary); } .bit-consensus{ flex:0 0 200px; border:1px solid var(--accent-moss-dim); background:var(--bg-raised); padding:10px 12px; } .bit-consensus-dir{ font-family:var(--font-heading); font-size:var(--text-xl); line-height:1.1; margin:2px 0; letter-spacing:var(--tracking-tight); } .bit-conf-track{ height:4px; background:var(--bg-sunken); margin:6px 0 4px; } .bit-conf-fill{ height:100%; } /* ================= gradio controls, restyled ================= */ .gradio-container .tabs > .tab-nav{ border-bottom:1px solid var(--border-default) !important; background:transparent !important; gap:0 !important; flex-wrap:wrap; margin-bottom:0 !important; } .gradio-container .tabs > .tab-nav > button{ font-family:var(--font-heading) !important; text-transform:uppercase; letter-spacing:var(--tracking-wide); font-size:var(--text-xs) !important; color:var(--text-tertiary) !important; background:transparent !important; border:none !important; border-bottom:2px solid transparent !important; padding:7px 12px !important; margin:0 !important; } .gradio-container .tabs > .tab-nav > button:hover{ color:var(--text-secondary) !important; } .gradio-container .tabs > .tab-nav > button.selected{ color:var(--text-primary) !important; border-bottom-color:var(--accent-amber) !important; } /* Field captions */ .block > label > span, .block > .form > label > span{ font-family:var(--font-tiny) !important; font-size:var(--text-2xs) !important; letter-spacing:var(--tracking-wider) !important; text-transform:uppercase; color:var(--text-tertiary) !important; } input, select, textarea{ font-family:var(--font-mono) !important; font-size:var(--text-xs) !important; background:var(--bg-sunken) !important; color:var(--text-primary) !important; border:1px solid var(--border-default) !important; padding:5px 8px !important; } input:focus, select:focus, textarea:focus{ outline:2px solid var(--focus-ring) !important; outline-offset:-1px; } /* Radio / checkbox options as segmented chips */ .gradio-container fieldset label, .gradio-container .wrap label:has(input[type="radio"]), .gradio-container .wrap label:has(input[type="checkbox"]){ background:transparent !important; border:1px solid var(--border-default) !important; color:var(--text-secondary) !important; padding:3px 8px !important; margin:2px !important; } .gradio-container fieldset label span, .gradio-container .wrap label:has(input[type="radio"]) span, .gradio-container .wrap label:has(input[type="checkbox"]) span{ font-family:var(--font-mono) !important; font-size:var(--text-2xs) !important; letter-spacing:var(--tracking-normal) !important; text-transform:none !important; color:var(--text-secondary) !important; opacity:1 !important; } .gradio-container fieldset label.selected, .gradio-container fieldset label:has(input:checked), .gradio-container .wrap label:has(input[type="radio"]:checked){ background:var(--accent-amber) !important; border-color:var(--accent-amber) !important; } .gradio-container fieldset label.selected span, .gradio-container fieldset label:has(input:checked) span, .gradio-container .wrap label:has(input[type="radio"]:checked) span{ color:var(--stone-950) !important; font-weight:500 !important; } .gradio-container input[type="radio"], .gradio-container input[type="checkbox"]{ accent-color:var(--accent-amber); } /* Accordions as the design's numbered sections */ .gradio-container .label-wrap{ font-family:var(--font-heading) !important; text-transform:uppercase; letter-spacing:var(--tracking-wide); font-size:var(--text-xs) !important; color:var(--text-primary) !important; border-bottom:1px solid var(--border-subtle) !important; padding:7px 0 !important; margin:0 !important; } .bit-accordion{ border:none !important; border-top:1px solid var(--border-subtle) !important; padding:0 !important; margin:0 0 2px !important; } /* Buttons -- base.css supplies the inverted hover treatment */ .bit-run-btn{ background:var(--accent-amber) !important; color:var(--stone-950) !important; font-family:var(--font-heading) !important; text-transform:uppercase; letter-spacing:var(--tracking-wide); border:1px solid var(--accent-amber) !important; font-weight:500 !important; font-size:var(--text-xs) !important; padding:7px 12px !important; } .bit-run-btn:hover{ background:var(--stone-950) !important; color:var(--accent-amber) !important; } .bit-ghost-btn{ background:transparent !important; color:var(--text-secondary) !important; border:1px solid var(--border-default) !important; font-family:var(--font-mono) !important; font-size:var(--text-2xs) !important; padding:5px 10px !important; } .bit-ghost-btn:hover{ background:var(--accent-amber) !important; color:var(--stone-950) !important; border-color:var(--accent-amber) !important; } /* Tables */ .bit-table table{ font-family:var(--font-mono) !important; font-size:var(--text-2xs) !important; font-variant-numeric:tabular-nums; } .bit-table thead th{ font-family:var(--font-tiny) !important; font-size:var(--text-2xs) !important; letter-spacing:var(--tracking-wider); text-transform:uppercase; color:var(--text-tertiary) !important; background:var(--bg-raised) !important; border-bottom:1px solid var(--border-default) !important; padding:5px 8px !important; } .bit-table tbody td{ border-color:var(--border-subtle) !important; padding:4px 8px !important; color:var(--text-secondary) !important; } /* Footer */ .bit-footer{ position:sticky; bottom:0; z-index:var(--z-header); display:flex; justify-content:space-between; gap:12px; background:var(--bg-panel); border-top:1px solid var(--border-default); padding:6px 12px; font-family:var(--font-mono); font-size:var(--text-2xs); color:var(--text-tertiary); } .bit-footer-right{ white-space:nowrap; } /* ================= page height ================= Gradio's fill_height wrapper sets `min-height:100%` on a child of a parent whose own height comes from its content. That is circular: the child asks for the parent's height, the parent grows to fit the child, repeat. Plotly's ResizeObserver then feeds the loop and the page grows as you scroll. This is a content-height dashboard, so the chain is cut instead. */ html, body{ height:auto !important; min-height:0 !important; } .gradio-container, gradio-app{ height:auto !important; min-height:0 !important; } .gradio-container main, .gradio-container .wrap, .gradio-container .contain{ height:auto !important; min-height:0 !important; } .gradio-container main.fillable{ min-height:0 !important; } /* A plot must never be able to grow its own container. */ .gradio-container .js-plotly-plot, .gradio-container .plot-container{ max-height:560px !important; } /* ================= sign-in ================= The LoginButton stays a plain, unwrapped Gradio component -- that DOM arrangement is the one that finally worked, and nothing here changes it. Only presentation. The button is pinned to the top-right of the header, and the header itself *reserves* that width via padding-right. Reserving is what keeps the status, glossary and store chips beside the button rather than under it: they are pushed left by the reserve, so they cannot collide however long the run context chip grows. An overlay with a guessed offset cannot promise that. */ :root{ --bit-header-h: 46px; /* keep in step with the header's own height */ --bit-header-pad: 12px; /* the header's own horizontal padding */ --bit-login-w: 200px; /* space reserved for the sign-in control */ --bit-stack-gap: 8px; /* the gap Gradio puts between stacked blocks */ } .gradio-container header{ padding-right: calc(var(--bit-header-pad) + var(--bit-login-w)) !important; } .bit-loginrow{ position:sticky; top:0; z-index:31; /* Lift by the header height *plus* the stack gap: the row starts one gap below the header, so ignoring it leaves the button sitting low. */ margin-top:calc(-1 * (var(--bit-header-h) + var(--bit-stack-gap))) !important; height:var(--bit-header-h); display:flex !important; align-items:center; /* same optical centre as the chips */ justify-content:flex-end; padding:0 var(--bit-header-pad) 0 0 !important; gap:0 !important; background:transparent !important; border:0 !important; pointer-events:none; /* must not eat header clicks */ } /* Gradio wraps the button in its own block. Without making that wrapper fill the bar and centre its child, `align-items` centres the *wrapper* and the button settles at its bottom edge -- which is the extra top space. */ .bit-loginrow > *{ pointer-events:auto; flex:0 0 auto !important; height:100% !important; min-height:0 !important; display:flex !important; align-items:center !important; padding:0 !important; margin:0 !important; } /* Colour and type only -- no positioning on the control itself. */ .bit-loginrow button{ background:var(--accent-amber) !important; color:var(--stone-950) !important; border:1px solid var(--accent-amber) !important; font-family:var(--font-mono) !important; font-size:var(--text-2xs) !important; padding:4px 10px !important; min-width:0 !important; white-space:nowrap; box-shadow:none !important; line-height:1.4 !important; /* Keep its own height and sit centred, rather than stretching to fill the bar -- a full-height button reads as a block, not a control. */ align-self:center !important; height:auto !important; min-height:0 !important; flex:0 0 auto !important; } .bit-loginrow button:hover{ background:var(--stone-950) !important; color:var(--accent-amber) !important; } @media (max-width: 1100px){ /* Too tight to overlay: drop it to its own slim strip and stop reserving. */ .gradio-container header{ padding-right:var(--bit-header-pad) !important; } .bit-loginrow{ position:static; margin-top:0 !important; height:auto; padding:4px var(--bit-header-pad) !important; border-bottom:1px solid var(--border-default) !important; background:var(--bg-panel) !important; } } /* ================= header help tooltip ================= The metrics glossary lives behind a `?` in the header. Pure CSS hover/focus, so there is no state to keep in sync and it works without JS. */ .bit-help{ position:relative; display:inline-flex; } .bit-help-dot{ display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px; cursor:help; border:1px solid var(--border-default); color:var(--text-tertiary); font-family:var(--font-mono); font-size:var(--text-2xs); } .bit-help-dot:hover, .bit-help-dot:focus{ border-color:var(--accent-amber); color:var(--accent-amber-strong); outline:none; } .bit-help-pop{ display:none; position:absolute; top:24px; right:0; z-index:var(--z-menu); width:320px; max-height:60vh; overflow:auto; padding:10px 12px; background:var(--bg-raised); border:1px solid var(--border-strong); box-shadow:0 8px 24px rgba(0,0,0,0.5); text-align:left; } .bit-help:hover .bit-help-pop, .bit-help:focus-within .bit-help-pop{ display:block; } /* Gradio centres the app in a padded `.contain` wrapper, which was costing 208px of a 1680px viewport. The design is a full-bleed dashboard, so the wrapper is stretched rather than the zones being shrunk to fit it. */ .gradio-container .contain, .gradio-container > .wrap, .gradio-container main, .gradio-container .main{ max-width:100% !important; width:100% !important; padding:0 !important; margin:0 !important; } /* ================= three zones, exact widths ================= The design uses