bit-backtest-lab / src /ui /theme.py
Bit-Trading-Company's picture
CI deploy b886e73b
14ba54c verified
Raw
History Blame Contribute Delete
29.8 kB
"""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 <aside style="width:286px"> and 306px, not proportions.
Gradio's Row/Column only offers relative `scale`, so the widths are pinned
here and the columns are stripped of their own padding and gap. */
.bit-zones{
gap:0 !important; flex-wrap:nowrap !important; align-items:stretch !important;
margin:0 !important;
}
.bit-zone-left{
flex:0 0 286px !important; width:286px !important; min-width:286px !important;
max-width:286px !important; padding:0 !important; gap:0 !important;
}
.bit-zone-center{
flex:1 1 auto !important; min-width:0 !important; padding:0 12px !important;
}
.bit-zone-left > *{ height:auto; }
/* The bridge target and trigger must stay in the DOM to receive events, but
never show. `visible=False` would remove them from the DOM entirely. */
#bit-action, #bit-trigger{
position:absolute !important; width:1px !important; height:1px !important;
overflow:hidden !important; clip:rect(0 0 0 0) !important; opacity:0 !important;
pointer-events:none !important;
}
@media (max-width: 1100px){
.bit-zones{ flex-wrap:wrap !important; }
.bit-zone-left{
flex:1 1 100% !important; width:100% !important; max-width:100% !important;
}
}
@media (max-width: 900px){
.bit-statband{ flex-direction:column; }
.bit-stat{ border-right:none; }
.bit-sig-grid{ flex-direction:column; }
.bit-consensus{ flex:1 1 auto; width:100%; }
}
"""
def full_css() -> str:
"""Fonts, the design system's globals, the Gradio reset, then the shell."""
return "\n".join([_font_face_css(), _global_css(), SELECTION_FIX,
GRADIO_RESET, SHELL_CSS])
def bit_theme() -> gr.Theme:
"""Gradio theme configured to the design's scale.
Setting Gradio's own spacing/radius/text variables is what stops its
defaults from injecting padding and gaps the design never asked for. Doing
it here rather than with `!important` overrides means the generated CSS is
already right instead of being corrected afterwards.
"""
return gr.themes.Base(
primary_hue=gr.themes.colors.yellow,
secondary_hue=gr.themes.colors.lime,
neutral_hue=gr.themes.colors.stone,
font=["Styrene A", "system-ui", "sans-serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
spacing_size=gr.themes.sizes.spacing_sm,
radius_size=gr.themes.sizes.radius_none,
text_size=gr.themes.sizes.text_sm,
).set(
# Palette
body_background_fill="#161512",
body_text_color="#f7f4ec",
background_fill_primary="#1d1c18",
background_fill_secondary="#24221d",
block_background_fill="transparent",
block_border_color="#3d3a32",
block_border_width="0px",
block_label_text_color="#6f6a56",
border_color_primary="#3d3a32",
button_primary_background_fill="#af9209",
button_primary_text_color="#161512",
button_secondary_background_fill="transparent",
button_secondary_text_color="#b6b09a",
input_background_fill="#000000",
input_border_color="#3d3a32",
# Spacing -- the design uses 12px panel padding and 6-10px gaps.
layout_gap="8px",
block_padding="0px",
form_gap_width="0px",
block_label_margin="0px",
block_title_padding="0px",
input_padding="5px 8px",
checkbox_label_padding="3px 8px",
checkbox_label_gap="6px",
button_small_padding="5px 10px",
button_large_padding="7px 12px",
# Type -- 10px workhorse, matching the design's dominant size.
body_text_size="10px",
input_text_size="10px",
block_label_text_size="8px",
block_title_text_size="10px",
button_small_text_size="8px",
button_large_text_size="10px",
checkbox_label_text_size="8px",
prose_text_size="10px",
# Square, everywhere.
block_radius="0px",
input_radius="0px",
button_large_radius="0px",
button_small_radius="0px",
checkbox_border_radius="0px",
container_radius="0px",
)
def static_paths() -> list[str]:
"""Directories Gradio may serve (fonts, tokens, mark)."""
return [str(ASSETS)]