Spaces:
Running on Zero
Running on Zero
| """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)] | |