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