"""Centralized design tokens and CSS injection for Readthrough. The palette is the published research note's, so the app and the document it serves are one design. dashboard/note_css.py is the source of truth for the values; the constants below restate them as literals because plotly cannot resolve a CSS variable. """ from __future__ import annotations import streamlit as st from dashboard.note_css import FONT_IMPORT, NOTE_CSS, TOKENS_LIGHT # ── Brand ───────────────────────────────────────────────────────────────────── BRAND_GREEN = "#1E6B55" BRAND_GREEN_DARK = "#134A3B" # ── Surfaces ────────────────────────────────────────────────────────────────── BG = "#F5F6F3" BG_MUTED = "#F5F6F3" BORDER = "#D5DAD4" BORDER_STRONG = "#8C968F" # ── Text ────────────────────────────────────────────────────────────────────── TEXT = "#15201B" TEXT_MUTED = "#3E4A44" TEXT_FAINT = "#8C968F" # ── Semantic palette — the note's own tokens ────────────────────────────────── # These are literals rather than var() references because plotly consumes them # directly and cannot resolve a CSS variable. Values track dashboard/note_css.py. GREEN = "#1E6B55" BULL = "#1E6B55" BULL_BG = "#DCEBE3" BULL_BORDER = "#1E6B55" RED = "#A63A2B" BEAR = "#A63A2B" BEAR_BG = "#F3DDD8" BEAR_BORDER = "#A63A2B" AMBER = "#B8791F" WARN = "#B8791F" WARN_BG = "#F4E8D2" WARN_BORDER = "#B8791F" BLUE = "#3E4A44" INFO = "#3E4A44" INFO_BG = "#ECEEE9" INFO_BORDER = "#D5DAD4" GRAY = "#3E4A44" PURPLE = "#1E6B55" AI_BG = "#DCEBE3" AI_BORDER = "#1E6B55" AI_COLOR = "#1E6B55" AI_BADGE_BG = "#DCEBE3" IMPACT_HIGH_BG = "#15201B" # near-white chip — highest contrast on dark IMPACT_HIGH_TEXT = "#F5F6F3" IMPACT_MED_BG = "#8C968F" # slate-400 IMPACT_MED_TEXT = "#F5F6F3" IMPACT_LOW_BG = "transparent" IMPACT_LOW_TEXT = "#8C968F" # slate-400 IMPACT_LOW_BORDER = "#8C968F" # slate-600 # ── Plotly rgba aliases ─────────────────────────────────────────────────────── GREEN_A53 = "rgba(30,107,85,0.55)" GREEN_A40 = "rgba(30,107,85,0.42)" RED_A40 = "rgba(166,58,43,0.42)" PURPLE_A53 = "rgba(19,74,59,0.55)" BLUE_A53 = "rgba(62,74,68,0.55)" # ── Radius ──────────────────────────────────────────────────────────────────── RADIUS_CARD = "12px" RADIUS_BTN = "8px" RADIUS_CHIP = "6px" # ── Typographic scale ───────────────────────────────────────────────────────── FS_PAGE = "1.55rem" # ~25px page/screen title FS_SECTION = "1.05rem" # ~17px section sub-header FS_CARD = "0.95rem" # ~15px card headline (semibold) FS_BODY = "0.9rem" # ~14.5px body text FS_META = "0.75rem" # ~12px meta/attribution line FS_EYEBROW = "0.6rem" # uppercase micro-label (was scattered 0.56–0.72rem) FS_SECTION_LABEL = "0.78rem" # ── Spacing scale ───────────────────────────────────────────────────────────── SPACE_1 = "4px" SPACE_2 = "8px" SPACE_3 = "12px" SPACE_4 = "16px" SPACE_5 = "20px" SPACE_6 = "28px" # intentionally 8px jump — used for section-break vertical rhythm # NAV_GROUPS now lives in dashboard.nav — imported from there in app.py. # Kept as a re-export for any legacy callers. def _lazy_nav_groups(): # noqa: F401 — accessed via dashboard.nav.NAV_GROUPS from dashboard.nav import NAV_GROUPS # noqa: F401 return NAV_GROUPS # ── Logo SVG — geometric P in emerald rounded square ───────────────────────── # The counter of the P is punched out in the canvas colour so the mark reads # correctly on the dark sidebar (it used to be punched out in white). LOGO_SVG = """ """ def _register_plotly_template() -> None: """Make dark the plotly default so charts inherit it without per-figure work. Individual figures still override plot/paper background and font colour; what this template actually buys is the styling nothing sets by hand — hover labels and the modebar, which otherwise render light-on-light. """ try: import plotly.graph_objects as go import plotly.io as pio except Exception: # plotly missing — charts are optional, styling is not fatal return # Already the default: nothing to do, and importantly nothing to validate. # Assigning it again makes plotly deep-copy and re-validate the template, # which reaches into numpy — see the guard in app.py for why that is a # hazard on a rerun. if pio.templates.default == "readthrough_paper": return if "readthrough_paper" not in pio.templates: pio.templates["readthrough_paper"] = go.layout.Template( layout=dict( paper_bgcolor=BG, plot_bgcolor=BG, font=dict(family="IBM Plex Sans", color=TEXT_MUTED, size=11), hoverlabel=dict( bgcolor="#ECEEE9", bordercolor=BORDER_STRONG, font=dict(family="IBM Plex Sans", color=TEXT, size=12), ), xaxis=dict(gridcolor="#ECEEE9", zerolinecolor="#D5DAD4", linecolor=BORDER, tickfont=dict(color=TEXT_MUTED)), yaxis=dict(gridcolor="#ECEEE9", zerolinecolor="#D5DAD4", linecolor=BORDER, tickfont=dict(color=TEXT_MUTED)), colorway=[BRAND_GREEN, BLUE, PURPLE, AMBER, RED, "#8C968F"], ) ) pio.templates.default = "readthrough_paper" def inject_global_css() -> None: """Inject the note's design system app-wide. Call once from app.py. Loads the three families the note is set in, defines its palette as CSS variables, ships the note stylesheet itself, then restates Streamlit's own chrome in those same variables so the app and the document it serves are one design rather than two. """ _register_plotly_template() st.html( """ """, )