"""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 = ( "" "" "" ) _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 , 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