SupraDashboard / src /viz /theme.py
Tianyi-Billy-Ma
Deploy: simplify codebase (dead-code removal, behavior-preserving)
1727091
Raw
History Blame Contribute Delete
36.5 kB
"""Theme and CSS tokens for SupraDashboard."""
from __future__ import annotations
import gradio as gr
# --------------------------------------------------------------------------- #
# Design tokens — Molecular macrocycle palette #
# Bone-white ground, copper + teal paired accents, Newsreader / Hanken Grotesk #
# / Space Mono type stack. All rule hues pass WCAG AA on #F4EFE6 (bone). #
# --------------------------------------------------------------------------- #
INK = "#2A2622"
MUTED = "#6B6258"
LINE = "#2A26222E"
HAIRLINE = "#2A262214"
SURFACE = "#FBF8F1"
APP_BG = "#F4EFE6"
PRIMARY = "#1E7A74" # teal
PRIMARY_DARK = "#155954" # deep teal
COPPER = "#B5642E"
COPPER_DARK = "#8F4A1E"
OK = "#1E7A4E"
WARN = "#9A6F12"
ERR = "#B42318"
# Rule-index → hue (idx mod len). Tuned for WCAG AA on bone-white #F4EFE6 / #FBF8F1.
# Color is redundant with bold weight + left-border + the literal N. number.
RULE_COLORS = ["#1F5FA8", "#7B4FA0", "#1E7A4E", "#B23A7E", COPPER_DARK, "#1E7A74"]
SERIF = '"Newsreader","Georgia","Times New Roman",serif'
MONO = '"Space Mono","IBM Plex Mono","SF Mono",ui-monospace,"Menlo",monospace'
BODY = '"Hanken Grotesk","ui-sans-serif","system-ui",sans-serif'
# --------------------------------------------------------------------------- #
# Theme + CSS — Molecular macrocycle #
# --------------------------------------------------------------------------- #
def _theme() -> gr.Theme:
return gr.themes.Base(
primary_hue=gr.themes.colors.teal,
secondary_hue=gr.themes.colors.orange,
neutral_hue=gr.themes.colors.stone,
radius_size=gr.themes.sizes.radius_lg,
font=(
gr.themes.GoogleFont("Hanken Grotesk"),
"ui-sans-serif",
"system-ui",
"sans-serif",
),
font_mono=(
gr.themes.GoogleFont("Space Mono"),
"ui-monospace",
"monospace",
),
).set(
# Pin all background/surface hues to bone-white so Gradio's dark-mode
# heuristics don't invert the palette in a default-light deployment.
body_background_fill=APP_BG,
body_background_fill_dark=APP_BG,
background_fill_primary=SURFACE,
background_fill_primary_dark=SURFACE,
background_fill_secondary=APP_BG,
background_fill_secondary_dark=APP_BG,
# Borders
block_border_color="#2A262220",
block_border_color_dark="#2A262220",
block_border_width="1px",
border_color_primary="#2A262222",
border_color_primary_dark="#2A262222",
# Input surfaces
input_background_fill=APP_BG,
input_background_fill_dark=APP_BG,
# Body text
body_text_color=INK,
body_text_color_dark=INK,
body_text_color_subdued=MUTED,
body_text_color_subdued_dark=MUTED,
block_label_text_color=INK,
block_label_text_color_dark=INK,
block_title_text_color=INK,
block_title_text_color_dark=INK,
# Primary button: teal
button_primary_background_fill=PRIMARY,
button_primary_background_fill_dark=PRIMARY,
button_primary_background_fill_hover=PRIMARY_DARK,
button_primary_background_fill_hover_dark=PRIMARY_DARK,
button_primary_text_color="#FBF8F1",
button_primary_text_color_dark="#FBF8F1",
# Secondary button: copper-tinted
button_secondary_background_fill=SURFACE,
button_secondary_background_fill_dark=SURFACE,
button_secondary_background_fill_hover="#F4EFE6",
button_secondary_border_color=COPPER,
button_secondary_text_color=COPPER,
)
_FONT_HEAD = (
"<link rel='preconnect' href='https://fonts.googleapis.com'/>"
"<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin/>"
"<link href='https://fonts.googleapis.com/css2?"
"family=Newsreader:ital,opsz,wght@0,6..72,300..600;1,6..72,400..500"
"&family=Hanken+Grotesk:wght@400;500;600;700"
"&family=Space+Mono:wght@400;700&display=swap' rel='stylesheet'/>"
)
_CSS = f"""
/* ------------------------------------------------------------------ */
/* Global reset + bone-white ground */
/* ------------------------------------------------------------------ */
.gradio-container {{
max-width: 1280px !important;
margin: 0 auto !important;
background: transparent !important;
font-family: "Hanken Grotesk", ui-sans-serif, system-ui, sans-serif !important;
color: {INK} !important;
}}
body, html {{
background: {APP_BG} !important;
color: {INK} !important;
}}
/* ------------------------------------------------------------------ */
/* Rotating heptagon watermark — fixed behind everything */
/* pointer-events:none so it never blocks interaction */
/* ------------------------------------------------------------------ */
body::before {{
content: '';
position: fixed;
inset: 0;
background-image:
radial-gradient(900px 600px at 50% 0%, #1E7A741F 0%, transparent 60%),
radial-gradient(700px 560px at 12% 6%, #1E7A7418 0%, transparent 65%),
radial-gradient(620px 580px at 94% 18%, #B5642E20 0%, transparent 62%),
radial-gradient(780px 640px at 68% 96%, #1E7A740E 0%, transparent 65%),
radial-gradient(560px 480px at 28% 92%, #B5642E0A 0%, transparent 62%);
z-index: 0;
pointer-events: none;
}}
/* Heptagon SVG watermark injected as a pseudo-element via a data URI */
body::after {{
content: '';
position: fixed;
top: 50%;
left: 50%;
width: 560px;
height: 560px;
transform: translate(-50%, -50%);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 700 700' fill='none'%3E%3Cpolygon points='350,28 602,149 664,422 490,640 210,640 36,422 98,149' stroke='%231E7A74' stroke-width='1.5' opacity='0.14'/%3E%3Cpolygon points='350,85 557,185 608,409 465,589 235,589 92,409 143,185' stroke='%23B5642E' stroke-width='1.5' opacity='0.11'/%3E%3Cpolygon points='350,153 504,227 542,394 436,528 264,528 158,394 196,227' stroke='%231E7A74' stroke-width='1.5' opacity='0.09'/%3E%3Ccircle cx='350' cy='350' r='80' stroke='%23B5642E' stroke-width='1.5' opacity='0.09'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 1;
pointer-events: none;
z-index: 0;
}}
@keyframes hept-spin {{
from {{ transform: translate(-50%, -50%) rotate(0deg); }}
to {{ transform: translate(-50%, -50%) rotate(360deg); }}
}}
/* Ensure all Gradio content sits above the watermark layer */
.gradio-container, footer {{
position: relative;
z-index: 1;
}}
/* ------------------------------------------------------------------ */
/* Page-load stagger — fills forwards so screenshot captures final */
/* ------------------------------------------------------------------ */
@keyframes mol-rise {{
from {{ opacity: 0; transform: translateY(10px); }}
to {{ opacity: 1; transform: none; }}
}}
@keyframes spin {{
to {{ transform: rotate(360deg); }}
}}
.spin {{
display: inline-block;
animation: spin 1s linear infinite;
}}
@media (prefers-reduced-motion: no-preference) {{
body::after {{ animation: hept-spin 90s linear infinite; }}
.app-head {{ animation: mol-rise .55s cubic-bezier(.2,.7,.2,1) .04s both; }}
.gradio-row {{ animation: mol-rise .55s cubic-bezier(.2,.7,.2,1) .14s both; }}
}}
/* ------------------------------------------------------------------ */
/* Header / brand */
/* ------------------------------------------------------------------ */
.app-head {{
border: 1px solid #2A262233;
border-radius: 18px;
background: {SURFACE};
padding: 22px 26px;
margin-bottom: 10px;
box-shadow: 0 1px 0 #fff inset, 0 12px 40px -16px #2A262233;
background-image: linear-gradient(90deg, #1E7A7414, transparent 40%, #B5642E14);
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 18px;
}}
.app-brand {{
display: flex;
align-items: center;
gap: 14px;
}}
.app-hept {{
width: 44px;
height: 44px;
flex-shrink: 0;
}}
.app-name {{
font-family: "Newsreader", Georgia, serif;
font-weight: 500;
font-size: 32px;
letter-spacing: -.02em;
color: {INK};
line-height: 1;
}}
.app-name span {{
color: {COPPER};
font-weight: 600;
}}
.app-sub {{
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif;
font-size: 11px;
letter-spacing: .05em;
color: {INK};
opacity: .55;
margin-top: 4px;
text-transform: uppercase;
}}
/* ------------------------------------------------------------------ */
/* Health strip dots */
/* ------------------------------------------------------------------ */
.health {{
display: flex;
gap: 20px;
flex-wrap: wrap;
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif;
font-size: 11px;
color: {MUTED};
align-items: baseline;
}}
.health .dot {{
display: inline-block;
width: 9px;
height: 9px;
border-radius: 50%;
margin-right: 7px;
vertical-align: middle;
box-shadow: 0 0 0 2px {SURFACE}, 0 0 0 3px #0001;
}}
/* ------------------------------------------------------------------ */
/* Run status line */
/* ------------------------------------------------------------------ */
.runstatus {{
border: 1px solid;
border-radius: 12px;
padding: 8px 16px;
margin: 4px 0 6px;
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif;
font-size: 13.5px;
font-weight: 600;
background: {SURFACE};
letter-spacing: .01em;
}}
.runstatus .g {{
font-size: 15px;
margin-right: 6px;
}}
/* ------------------------------------------------------------------ */
/* Prediction card */
/* ------------------------------------------------------------------ */
.pred-card {{
border: 1px solid {LINE};
border-radius: 16px;
background: {SURFACE};
padding: 6px 6px 8px;
box-shadow: 0 1px 0 #fff inset, 0 2px 10px -4px #2A26222E, 0 8px 28px -14px #2A262226;
}}
.pred-title {{
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif;
font-size: 10px;
letter-spacing: .16em;
text-transform: uppercase;
color: {MUTED};
font-weight: 700;
padding: 8px 14px 6px;
}}
.pred-card .row {{
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 14px;
border-radius: 10px;
}}
.pred-card .row .lbl {{
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif;
font-size: 14px;
color: {MUTED};
}}
.pred-card .row .val {{
font-family: {MONO};
font-size: 18px;
letter-spacing: -.01em;
font-weight: 500;
color: {INK};
font-variant-numeric: tabular-nums;
}}
/* Combined row: copper warm tint */
.pred-card .row.primary {{
background: linear-gradient(90deg, #B5642E14, transparent);
border-radius: 10px;
}}
.pred-card .row.primary .lbl {{
font-weight: 700;
color: {COPPER_DARK};
font-size: 14.5px;
}}
.pred-card .row.primary .val {{
font-weight: 700;
font-size: 24px;
color: {COPPER};
}}
.dots {{ color: {MUTED}; letter-spacing: .1em; }}
/* ------------------------------------------------------------------ */
/* TL;DR cards */
/* ------------------------------------------------------------------ */
.tldr {{
border: 1px solid {LINE};
border-radius: 16px;
background: {SURFACE};
padding: 13px 15px;
box-shadow: 0 1px 0 #fff inset, 0 2px 10px -4px #2A26222E, 0 8px 28px -14px #2A262226;
}}
.tldr {{
overflow: hidden;
word-break: break-word;
}}
.tldr-tag {{
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif;
font-size: 9.5px;
letter-spacing: .14em;
text-transform: uppercase;
font-weight: 700;
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}}
.tldr-body {{
font-family: {SERIF};
font-size: 14.5px;
line-height: 1.58;
color: {INK};
}}
/* ------------------------------------------------------------------ */
/* Reasoning hero panel */
/* ------------------------------------------------------------------ */
.reason-head {{
font-family: "Newsreader", Georgia, serif;
font-style: italic;
font-size: 15px;
color: {MUTED};
margin: 4px 0 8px;
letter-spacing: .01em;
}}
.reason-wrap {{
border: 1px solid {LINE};
border-radius: 18px;
background: {SURFACE};
padding: 24px 28px;
box-shadow: 0 1px 0 #fff inset, 0 12px 40px -16px #2A262233;
min-height: 440px;
border-top: 2px solid #B5642E33;
background-image:
radial-gradient(420px 320px at 50% 0%, #1E7A740E, transparent 70%),
linear-gradient(160deg, #1E7A7408, transparent 40%, #B5642E06);
}}
.reason-body {{
font-family: {SERIF};
font-size: 16.5px;
line-height: 1.72;
color: {INK};
white-space: normal;
}}
.reason-body .prose {{
margin: .4em 0;
font-family: {SERIF};
font-size: 16.5px;
line-height: 1.72;
}}
/* Rule rows: circular node + colored title + ink explanation */
.reason-body .rule {{
display: flex;
align-items: flex-start;
gap: 0;
padding: 10px 0 10px 0;
border-bottom: 1px solid {HAIRLINE};
/* border-left is set inline per rule — required by smoke test */
}}
.reason-body .rule:last-of-type {{ border-bottom: none; }}
.rule-node {{
/* Circular numbered badge — color/bg set inline per rule */
line-height: 1 !important;
margin-top: 2px;
}}
.reason-empty {{
border-radius: 16px;
padding: 56px 20px;
text-align: center;
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif;
font-size: 14px;
background: {SURFACE};
color: {MUTED};
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 320px;
line-height: 1.6;
}}
.reason-guide {{
margin-top: 12px;
font-size: 13px;
color: {INK};
}}
.reason-guide span {{
color: {MUTED};
}}
/* ------------------------------------------------------------------ */
/* Expert review band */
/* ------------------------------------------------------------------ */
.review-band {{
border: 1px solid {LINE} !important;
border-top: 3px solid {COPPER} !important;
border-radius: 16px !important;
background: {SURFACE} !important;
padding: 20px 22px 22px !important;
margin-top: 18px !important;
box-shadow: 0 1px 0 #fff inset, 0 10px 32px -18px #2A262233 !important;
gap: 6px !important;
}}
.review-q {{
font-family: "Newsreader", Georgia, serif;
font-size: 18px;
font-weight: 500;
color: {INK};
margin: 8px 0 4px;
letter-spacing: -.005em;
}}
.ack {{
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif;
font-size: 13.5px;
padding: 9px 14px;
border-radius: 12px;
display: inline-block;
}}
.ack.ok {{ color: {OK}; background: #1E7A4E1F; }}
.ack.warn {{ color: {WARN}; background: #9A6F1212; }}
/* ------------------------------------------------------------------ */
/* Radio (rating scale) — horizontal pill chips */
/* ------------------------------------------------------------------ */
.rating-radio .wrap {{ gap: 7px; flex-wrap: wrap; }}
.rating-radio label,
.pill-toggle label {{
border: 1px solid #2A262240 !important;
padding: 6px 12px !important;
border-radius: 999px !important;
background: {SURFACE} !important;
color: {INK} !important;
transition: background .12s ease, color .12s ease, border-color .12s ease !important;
}}
.rating-radio label:hover:not(:has(input:checked)),
.pill-toggle label:hover:not(:has(input:checked)) {{
border-color: {COPPER} !important;
background: #B5642E0A !important;
}}
.rating-radio label:has(input:checked),
.pill-toggle label:has(input:checked) {{
background: {PRIMARY} !important;
color: #ffffff !important;
border-color: {PRIMARY} !important;
}}
/* Selected pill: force strong white text on the highlight fill. Gradio renders
the label text inside a child <span>, which otherwise keeps the dark INK
colour and reads poorly on the green/copper fill. */
.rating-radio label:has(input:checked) span,
.pill-toggle label:has(input:checked) span {{
color: #ffffff !important;
font-weight: 600 !important;
}}
.rating-radio label span {{
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif !important;
font-size: 13px !important;
}}
.prompt-modes .wrap {{
flex-direction: column !important;
gap: 6px !important;
align-items: stretch !important;
}}
.prompt-modes label {{
display: block !important;
width: 100% !important;
padding: 8px 14px !important;
border: 1.5px solid {LINE} !important;
border-radius: 8px !important;
cursor: pointer !important;
transition: background 0.15s, border-color 0.15s !important;
}}
.prompt-modes label:has(input:checked) {{
background: {PRIMARY} !important;
border-color: {PRIMARY} !important;
color: white !important;
font-weight: 600 !important;
}}
/* Run control: a REAL Bootstrap-style split button — main "Run" pill + caret
toggle fused via a flex btn-group (shared border, caret has no left radius).
gr.Button renders <button> with no wrapper, so the buttons are direct flex
children of the .run-group row and join seamlessly. */
.run-col {{ position: relative !important; gap: 6px !important; justify-content: flex-end !important; padding-top: 22px !important; }}
/* The group is the pill; the two buttons fuse inside it. Selectors MUST be more
specific than `button.primary` (which forces border-radius/box-shadow with
!important), so we qualify with `.run-group button.run-*`. */
.run-group {{
display: flex !important; flex-wrap: nowrap !important; gap: 0 !important; column-gap: 0 !important;
width: 100% !important; align-items: stretch !important;
border-radius: 12px !important; box-shadow: 0 6px 16px -8px #1E7A7488 !important;
}}
.run-group button.run-main, .run-group button.run-caret {{
margin: 0 !important; box-shadow: none !important; transform: none !important;
}}
.run-group button.run-main {{
flex: 1 1 auto !important;
border-radius: 12px 0 0 12px !important;
border-right: 1px solid rgba(255,255,255,.4) !important;
}}
.run-group button.run-caret {{
flex: 0 0 38px !important; width: 38px !important; min-width: 38px !important; max-width: 38px !important;
padding: 0 !important; border-radius: 0 12px 12px 0 !important; font-size: 13px !important; line-height: 1 !important;
}}
/* Dropdown menu under the group (Use cache / Force re-run) */
.run-menu {{
position: absolute !important; top: calc(100% + 4px) !important; left: 0 !important; right: 0 !important;
z-index: 60 !important; background: {SURFACE} !important; border: 1px solid {LINE} !important;
border-radius: 10px !important; box-shadow: 0 14px 32px -12px rgba(42,38,34,.3) !important;
padding: 5px !important; gap: 2px !important;
}}
/* run-menu-item is the class ON the <button> (gr.Button), so target
`button.run-menu-item` — NOT `.run-menu-item button` (matches nothing, which
left the default light text on the cream menu = unreadable). */
.run-menu button.run-menu-item, button.run-menu-item {{
width: 100% !important; background: transparent !important; color: {INK} !important;
border: none !important; box-shadow: none !important; text-align: left !important;
justify-content: flex-start !important; padding: 8px 12px !important; border-radius: 7px !important;
font-weight: 500 !important; font-size: 13px !important;
}}
.run-menu button.run-menu-item:hover, button.run-menu-item:hover {{
background: {APP_BG} !important; color: {COPPER_DARK} !important;
}}
/* ------------------------------------------------------------------ */
/* Gradio component overrides — keep chrome consistent */
/* ------------------------------------------------------------------ */
/* Labels — sentence case, quiet; values outrank their labels */
.gradio-container label span,
.gradio-container .label-wrap span {{
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif !important;
font-size: 12px !important;
letter-spacing: .01em !important;
text-transform: none !important;
color: {MUTED} !important;
font-weight: 600 !important;
}}
/* Dropdowns + inputs */
.gradio-container select,
.gradio-container input,
.gradio-container textarea {{
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif !important;
color: {INK} !important;
background: {APP_BG} !important;
border-color: #2A262222 !important;
border-radius: 12px !important;
}}
/* Guest structure image container — heptagon-framed border */
.gradio-container .image-container,
.gradio-container .image-frame {{
border: 1.5px solid #B5642E44 !important;
border-radius: 16px !important;
background: {SURFACE} !important;
box-shadow: 0 0 0 4px #1E7A7408, 0 2px 10px -4px #2A26222E, 0 8px 28px -14px #2A262226 !important;
overflow: hidden !important;
}}
/* Block cards */
.gradio-container .block {{
background: {SURFACE} !important;
border-color: {LINE} !important;
border-radius: 18px !important;
margin-top: 8px !important;
margin-bottom: 8px !important;
}}
/* Primary button: teal with copper hover-shimmer */
.gradio-container .btn-primary, button.primary {{
background: linear-gradient(180deg, {PRIMARY}, {PRIMARY_DARK}) !important;
color: #FBF8F1 !important;
border: none !important;
border-radius: 12px !important;
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif !important;
font-weight: 700 !important;
font-size: 14px !important;
letter-spacing: .02em !important;
box-shadow: 0 6px 18px -8px #1E7A7488, 0 1px 0 #ffffff28 inset !important;
transition: transform .12s ease, box-shadow .12s ease !important;
}}
.gradio-container .btn-primary:hover, button.primary:hover {{
transform: translateY(-1px) !important;
box-shadow: 0 10px 24px -8px #1E7A74AA, 0 1px 0 #ffffff28 inset !important;
}}
/* Tab styling */
.gradio-container .tabs .tab-nav button {{
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif !important;
font-weight: 600 !important;
font-size: 13.5px !important;
letter-spacing: .02em !important;
text-transform: none !important;
color: {MUTED} !important;
border-radius: 10px 10px 0 0 !important;
}}
.gradio-container .tabs .tab-nav button.selected {{
color: {PRIMARY} !important;
border-bottom-color: {PRIMARY} !important;
}}
.gradio-container .tabs .tab-nav button:hover {{
color: {INK} !important;
}}
/* Submit review — the page's primary deliverable action: filled copper CTA */
.copper-btn, button.copper-btn {{
background: linear-gradient(180deg, {COPPER}, {COPPER_DARK}) !important;
color: #FBF8F1 !important;
border: none !important;
border-radius: 12px !important;
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif !important;
font-weight: 700 !important;
font-size: 14px !important;
letter-spacing: .02em !important;
width: auto !important;
min-width: 190px !important;
align-self: flex-end !important;
box-shadow: 0 6px 18px -8px #B5642E77, 0 1px 0 #ffffff30 inset !important;
transition: transform .12s ease, box-shadow .12s ease !important;
}}
.copper-btn:hover, button.copper-btn:hover {{
transform: translateY(-1px) !important;
background: linear-gradient(180deg, {COPPER}, {COPPER_DARK}) !important;
box-shadow: 0 10px 24px -8px #B5642EAA, 0 1px 0 #ffffff30 inset !important;
}}
/* ── Structure viewer (Host|Guest × 2D|3D, inline source, legend) ── */
.viz-controls {{ align-items: center !important; gap: 10px !important; flex-wrap: wrap !important; }}
/* Discovery Board: one consolidated control panel (filters · feature filter ·
sort · view) — a single bordered card, replacing three loose stacked rows. */
.board-controls {{
border: 1px solid {LINE} !important;
border-radius: 14px !important;
background: {SURFACE} !important;
padding: 12px 14px !important;
margin: 6px 0 12px !important;
box-shadow: 0 1px 0 #fff inset, 0 6px 22px -14px #2A262233 !important;
display: flex !important; flex-direction: column !important; gap: 9px !important;
}}
.board-controls > .viz-controls {{ margin: 0 !important; gap: 12px !important; align-items: flex-end !important; }}
/* the inner control blocks shouldn't each draw their own card border */
.board-controls .block {{ border: none !important; background: transparent !important; box-shadow: none !important; margin: 0 !important; }}
/* compact number fields (feature min/max AND the slider's value box). Center the
digits and give them room — the Min-logKa slider box was clipping its number. */
.board-controls input[type=number] {{
height: 38px !important; border-radius: 10px !important;
text-align: center !important; padding: 0 8px !important; min-width: 58px !important;
}}
/* Gradio renders the slider's editable value as a small number box in the slider
header; widen + center it so the value isn't cut off or overlapped. */
.board-controls .gr-slider input[type=number],
.board-controls input[aria-label*="logKa"],
.board-controls .head_container input {{
text-align: center !important; min-width: 56px !important; width: 56px !important;
}}
/* The radios are <fieldset class="block pill-toggle"> with an inline
min-width:min(160px,100%); override it so Host|Guest and 2D|3D shrink to
content and both stay visible instead of overflowing/clipping. */
.viz-controls .pill-toggle {{ flex: 0 0 auto !important; min-width: 0 !important; width: auto !important; }}
/* 2D card: white mat, quiet frame, rounded — now an HTML card (build_2d_html)
that carries the SAME in-view caption bar as the 3D viewer. */
.struct-2d {{ border: 1px solid {HAIRLINE} !important; border-radius: 16px !important; background: #ffffff !important; box-shadow: 0 1px 3px {LINE}, 0 8px 24px -12px #2A262222 !important; overflow: hidden !important; }}
.struct-2d img {{ display: block !important; max-width: 100% !important; max-height: 320px !important; width: auto !important; height: auto !important; margin: 0 auto !important; object-fit: contain !important; image-rendering: -webkit-optimize-contrast; }}
/* 3D frame: round the iframe itself (no clipping parent that eats controls) */
.struct-3d {{ border: 1px solid {HAIRLINE} !important; border-radius: 16px !important; background: #ffffff !important; overflow: visible !important; box-shadow: 0 1px 3px {LINE}, 0 8px 24px -12px #2A262222 !important; }}
.struct-3d iframe {{ width: 100% !important; border: none !important; border-radius: 16px !important; display: block !important; }}
/* ── Candidates grid (discovery board): explicit light styling — without it
the Gradio Dataframe falls back to its dark default — unreadable on the light theme ── */
/* Auto-fit: columns size to content, each cell one line; the .table-wrap scrolls
horizontally when the table is wider than the panel. (gr.Dataframe also exposes
drag-resizable column headers.)
IMPORTANT: do NOT use `width: max-content` here. Gradio's table lives inside a
`.table` flex child; `max-content` made it balloon to ~1e6px (every cell went
invisible). `width: auto; min-width: 100%` lets columns size to content, fill the
panel when narrow, and overflow into the scrolling wrap when wide. */
/* Column auto-fit ("wrapping"): columns size to content, each cell one line, and
the table overflows into a horizontal scroll when wider than the panel.
NOTE: this does NOT control vertical paging. Gradio's Dataframe renders exactly
the rows that fit `max_height` and does not scroll past them (confirmed with a
minimal standalone repro — virtual scroll is a no-op in our gradio<5.50). So the
Discovery Board sizes `max_height` to the selected row count and a "Rows"
selector caps how many top-ranked records are shown. */
/* Styling only — let Gradio own the table layout + its single .table-wrap
horizontal scroll. Earlier width/table-layout/overflow overrides fought that and
caused either a duplicate scrollbar or blank right-hand columns. */
#candidates-grid table {{ font-family: "Hanken Grotesk", sans-serif; font-size: 13.5px; color: {INK}; }}
/* ── Pager (below Discovery Board grid) ── */
.pager-row {{
align-items: center !important;
justify-content: center !important;
gap: 10px !important;
flex-wrap: nowrap !important;
padding: 12px 0 4px !important;
margin-top: 0 !important;
border-top: 1px solid {HAIRLINE} !important;
}}
/* Gradio sizes each child to width:100% (the Markdown block was 1216px wide,
shoving the buttons to the row edges). Force every child to content width so
justify-content:center clusters them in the middle. */
.pager-row > * {{ flex: 0 0 auto !important; width: auto !important; min-width: 0 !important; }}
/* Prev / Next pill ghost buttons */
.pager-row button {{
height: 42px !important;
min-width: 0 !important;
width: auto !important;
padding: 0 18px !important;
border-radius: 999px !important;
border: 1px solid #B5642E59 !important;
background: {SURFACE} !important;
color: {COPPER} !important;
font-family: {BODY} !important;
font-size: 13px !important;
font-weight: 600 !important;
letter-spacing: .02em !important;
box-shadow: none !important;
transition: background .13s ease, color .13s ease, border-color .13s ease, transform .1s ease !important;
line-height: 1 !important;
}}
.pager-row button:hover {{
background: linear-gradient(180deg, {COPPER}, {COPPER_DARK}) !important;
border-color: {COPPER_DARK} !important;
color: {SURFACE} !important;
transform: translateY(-1px) !important;
}}
.pager-row button:disabled {{
opacity: 0.38 !important;
cursor: not-allowed !important;
transform: none !important;
}}
/* Page number input: compact mono digit field, no label overhead */
#pager-input {{
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
min-width: 0 !important;
width: 64px !important;
flex: 0 0 64px !important;
}}
#pager-input .label-wrap {{ display: none !important; }}
#pager-input input[type=number] {{
width: 64px !important;
height: 42px !important;
padding: 0 8px !important;
border-radius: 8px !important;
border: 1px solid #B5642E59 !important;
background: {APP_BG} !important;
color: {INK} !important;
font-family: {MONO} !important;
font-size: 16px !important;
font-weight: 400 !important;
font-variant-numeric: tabular-nums !important;
text-align: center !important;
-moz-appearance: textfield !important;
}}
#pager-input input[type=number]::-webkit-inner-spin-button,
#pager-input input[type=number]::-webkit-outer-spin-button {{
-webkit-appearance: none !important; margin: 0 !important;
}}
#pager-input input[type=number]:focus {{
outline: none !important;
border-color: {COPPER} !important;
box-shadow: 0 0 0 3px #B5642E1A !important;
}}
/* "of N · M records" label: mono numbers, muted prose. NOTE: keep the <p> as
inline/normal flow (NOT flex) — flex on the paragraph collapses the spaces
between "of", the bold count and "records". */
.pager-row .pager-total {{
display: flex !important;
align-items: center !important;
margin: 0 6px !important;
}}
.pager-row .pager-total p {{
color: {MUTED} !important;
font-family: {BODY} !important;
font-size: 15px !important;
white-space: nowrap !important;
margin: 0 !important;
line-height: 1 !important;
}}
.pager-row .pager-total strong {{
font-family: {MONO} !important;
font-size: 15px !important;
font-weight: 600 !important;
color: {INK} !important;
font-variant-numeric: tabular-nums !important;
}}
#candidates-grid td, #candidates-grid th {{ padding: 9px 12px !important; border-bottom: 1px solid {HAIRLINE} !important; border-right: none !important; white-space: nowrap !important; }}
#candidates-grid td .cell-wrap, #candidates-grid td .text {{ white-space: nowrap !important; overflow: visible !important; }}
#candidates-grid thead th {{ background: {SURFACE} !important; border-bottom: 1px solid {LINE} !important; }}
#candidates-grid thead th .header-button,
#candidates-grid thead th .header-button span,
#candidates-grid thead th .header-content span {{
color: {MUTED} !important; background: transparent !important;
font-family: {MONO} !important; font-size: 10.5px !important; font-weight: 600 !important;
letter-spacing: .02em !important; text-transform: none !important; cursor: default !important;
}}
#candidates-grid thead th .header-button {{ background: transparent !important; border: none !important; box-shadow: none !important; }}
/* Per-column "â‹®" menu button (sort / filter flyout). Kept visible but quiet:
transparent square, muted glyph, soft copper hover. */
#candidates-grid .cell-menu-button {{
background: transparent !important; color: {MUTED} !important;
border: none !important; box-shadow: none !important; outline: none !important;
border-radius: 6px !important; min-width: 0 !important; width: 20px !important;
padding: 0 !important;
}}
#candidates-grid .cell-menu-button:hover {{ background: {HAIRLINE} !important; color: {COPPER_DARK} !important; }}
/* Numeric cols: # (1), Predicted logKa (4), True logKa (5) — mono, right-aligned
(a Host column was inserted at col 3, shifting the numeric columns right by one). */
#candidates-grid td:nth-child(1), #candidates-grid td:nth-child(4), #candidates-grid td:nth-child(5) {{ text-align: right !important; }}
#candidates-grid td:nth-child(1) .text, #candidates-grid td:nth-child(4) .text, #candidates-grid td:nth-child(5) .text {{
font-family: {MONO} !important; font-variant-numeric: tabular-nums;
}}
#candidates-grid td:nth-child(6) .text {{ font-family: {MONO} !important; font-size: 11px !important; }} /* SMILES */
#candidates-grid tbody tr td {{ background: {SURFACE} !important; transition: background-color .12s ease; }}
#candidates-grid tbody tr:nth-child(even) td {{ background: #F8F3EA !important; }}
#candidates-grid tbody tr {{ cursor: pointer; }}
#candidates-grid tbody tr:hover td {{ background: #F1E9DB !important; }}
/* Only remove the vertical cap (the old 62vh re-added a vertical scrollbar and
fought the pager). Leave horizontal scroll to Gradio's native .table-wrap/table
pair — it syncs them on drag; overriding their overflow desynced them and blanked
the right-hand columns. Pagination keeps each page short, so no vertical scroll. */
#candidates-grid .table-wrap {{ max-height: none !important; }}
/* Quiet, compact Refresh button (was a full-width dark default 'secondary') */
.refresh-btn, button.refresh-btn {{
width: auto !important; min-width: 110px !important; max-width: 150px !important;
align-self: flex-start !important; flex: 0 0 auto !important;
background: {SURFACE} !important; color: {INK} !important;
border: 1px solid {LINE} !important; box-shadow: none !important;
border-radius: 10px !important; font-weight: 600 !important; font-size: 13px !important;
}}
.refresh-btn:hover, button.refresh-btn:hover {{ background: {APP_BG} !important; border-color: {COPPER} !important; color: {COPPER_DARK} !important; }}
/* ── Column chooser (Label-Studio-style multiselect over the discovery board) ── */
.col-chooser {{ margin-top: 4px !important; }}
.col-chooser .wrap-inner, .col-chooser input {{ background: {APP_BG} !important; }}
/* Selected-column chips: copper-tinted tokens so the active view reads at a glance */
.col-chooser .token, .col-chooser .secondary-wrap .token {{
background: #B5642E16 !important; color: {COPPER_DARK} !important;
border: 1px solid #B5642E3A !important; border-radius: 999px !important;
font-family: "Hanken Grotesk", ui-sans-serif, sans-serif !important;
font-size: 12px !important; font-weight: 600 !important;
}}
.col-chooser .token .token-remove {{ color: {COPPER_DARK} !important; }}
/* ── Prompt preview ── */
.ph {{ font-family: {MONO}; color: {COPPER}; background: #B5642E12; padding: 0 4px; border-radius: 4px; font-size: 12.5px; }}
.user-prompt-view {{ font-size: 13px; color: {INK}; line-height: 1.6; }}
.user-prompt-view table {{ width: 100%; border-collapse: collapse; margin: 8px 0; }}
.user-prompt-view td {{ padding: 4px 8px; border-bottom: 1px solid {HAIRLINE}; }}
.user-prompt-view td:first-child {{ color: {INK}; }}
.user-prompt-view td:last-child {{ font-family: {MONO}; }}
/* Footer */
footer {{ display: none !important; }}
"""