Spaces:
Sleeping
Sleeping
| """ | |
| theme.py β SchemeImpactNet shared design system | |
| Editorial / policy-brief aesthetic. | |
| Fonts: Fraunces (display) + Source Serif 4 (body) + DM Mono (data/labels) | |
| Palette: warm off-white #FAF9F7, deep stone #1C1917, saffron accent #FB923C | |
| """ | |
| THEME_CSS = """ | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,600;0,9..144,700;1,9..144,300&family=Source+Serif+4:ital,opsz,wght@0,8..60,300;0,8..60,400;0,8..60,600&family=DM+Mono:wght@400;500&display=swap'); | |
| html, body, [class*="css"] { | |
| font-family: 'Source Serif 4', Georgia, serif !important; | |
| } | |
| .stApp { | |
| background-color: #FAF9F7 !important; | |
| } | |
| #MainMenu, footer, header { visibility: hidden; } | |
| .block-container { | |
| padding: 2rem 2.5rem 3rem !important; | |
| max-width: 1320px !important; | |
| } | |
| /* ββ Sidebar ββ */ | |
| [data-testid="stSidebar"] { | |
| background: #1C1917 !important; | |
| border-right: none !important; | |
| } | |
| [data-testid="stSidebarContent"] { | |
| background: #1C1917 !important; | |
| } | |
| /* Nav links generated by st.navigation */ | |
| [data-testid="stSidebarNavLink"] { | |
| border-radius: 5px !important; | |
| padding: 0.5rem 1rem !important; | |
| font-family: 'DM Mono', monospace !important; | |
| font-size: 0.7rem !important; | |
| letter-spacing: 0.5px !important; | |
| color: #A8A29E !important; | |
| text-decoration: none !important; | |
| transition: all 0.15s ease !important; | |
| border-left: 2px solid transparent !important; | |
| } | |
| [data-testid="stSidebarNavLink"]:hover { | |
| background: rgba(251,146,60,0.1) !important; | |
| color: #FB923C !important; | |
| border-left-color: rgba(251,146,60,0.4) !important; | |
| } | |
| [data-testid="stSidebarNavLink"][aria-current="page"] { | |
| background: rgba(251,146,60,0.15) !important; | |
| color: #FB923C !important; | |
| border-left-color: #FB923C !important; | |
| } | |
| /* ββ Typography ββ */ | |
| h1, h2, h3 { | |
| font-family: 'Fraunces', serif !important; | |
| color: #1C1917 !important; | |
| } | |
| h1 { font-size: 2.2rem !important; font-weight: 600 !important; line-height: 1.15 !important; } | |
| h2 { font-size: 1.5rem !important; font-weight: 600 !important; } | |
| h3 { font-size: 1.1rem !important; font-weight: 600 !important; } | |
| p { font-family: 'Source Serif 4', serif !important; color: #292524 !important; } | |
| /* ββ Metric cards ββ */ | |
| [data-testid="stMetric"] { | |
| background: #FFFFFF !important; | |
| border: 1px solid #E7E5E4 !important; | |
| border-radius: 8px !important; | |
| padding: 1rem 1.2rem !important; | |
| } | |
| [data-testid="stMetricLabel"] p { | |
| font-family: 'DM Mono', monospace !important; | |
| font-size: 0.62rem !important; | |
| letter-spacing: 2px !important; | |
| text-transform: uppercase !important; | |
| color: #78716C !important; | |
| } | |
| [data-testid="stMetricValue"] { | |
| font-family: 'Fraunces', serif !important; | |
| font-size: 1.85rem !important; | |
| font-weight: 600 !important; | |
| color: #1C1917 !important; | |
| line-height: 1.2 !important; | |
| } | |
| [data-testid="stMetricDelta"] { | |
| font-family: 'DM Mono', monospace !important; | |
| font-size: 0.7rem !important; | |
| } | |
| /* ββ Inputs ββ */ | |
| [data-testid="stSelectbox"] label p, | |
| [data-testid="stSlider"] label p, | |
| [data-testid="stTextInput"] label p, | |
| [data-testid="stMultiSelect"] label p { | |
| font-family: 'DM Mono', monospace !important; | |
| font-size: 0.65rem !important; | |
| letter-spacing: 1.5px !important; | |
| text-transform: uppercase !important; | |
| color: #78716C !important; | |
| } | |
| /* ββ Buttons ββ */ | |
| .stButton > button { | |
| font-family: 'DM Mono', monospace !important; | |
| font-size: 0.7rem !important; | |
| letter-spacing: 1px !important; | |
| text-transform: uppercase !important; | |
| background: #1C1917 !important; | |
| color: #FAF9F7 !important; | |
| border: none !important; | |
| border-radius: 6px !important; | |
| padding: 0.5rem 1.2rem !important; | |
| } | |
| .stButton > button:hover { | |
| background: #FB923C !important; | |
| color: #1C1917 !important; | |
| } | |
| /* ββ Dataframes ββ */ | |
| [data-testid="stDataFrame"] { | |
| border: 1px solid #E7E5E4 !important; | |
| border-radius: 8px !important; | |
| overflow: hidden !important; | |
| } | |
| [data-testid="stDataFrame"] th { | |
| font-family: 'DM Mono', monospace !important; | |
| font-size: 0.65rem !important; | |
| letter-spacing: 1px !important; | |
| text-transform: uppercase !important; | |
| background: #F5F5F4 !important; | |
| color: #57534E !important; | |
| } | |
| /* ββ Expander ββ */ | |
| [data-testid="stExpander"] { | |
| border: 1px solid #E7E5E4 !important; | |
| border-radius: 8px !important; | |
| background: #FFFFFF !important; | |
| } | |
| details summary p { | |
| font-family: 'DM Mono', monospace !important; | |
| font-size: 0.72rem !important; | |
| letter-spacing: 0.5px !important; | |
| color: #57534E !important; | |
| } | |
| /* ββ Alerts ββ */ | |
| [data-testid="stAlert"] { | |
| border-radius: 8px !important; | |
| } | |
| /* ββ Caption ββ */ | |
| [data-testid="stCaptionContainer"] p { | |
| font-family: 'DM Mono', monospace !important; | |
| font-size: 0.63rem !important; | |
| color: #A8A29E !important; | |
| letter-spacing: 0.3px !important; | |
| } | |
| /* ββ Divider ββ */ | |
| hr { | |
| border: none !important; | |
| border-top: 1px solid #E7E5E4 !important; | |
| margin: 1.5rem 0 !important; | |
| } | |
| /* ββ Tab strip ββ */ | |
| [data-testid="stTabs"] [role="tab"] { | |
| font-family: 'DM Mono', monospace !important; | |
| font-size: 0.68rem !important; | |
| letter-spacing: 1px !important; | |
| text-transform: uppercase !important; | |
| } | |
| </style> | |
| """ | |
| # ββ Plotly shared layout (light, editorial) βββββββββββββββββββββββββββββββββββ | |
| PLOTLY_LAYOUT = dict( | |
| paper_bgcolor="#FFFFFF", | |
| plot_bgcolor="#FAFAF9", | |
| font=dict(family="DM Mono, monospace", color="#292524", size=10.5), | |
| margin=dict(l=0, r=0, t=44, b=0), | |
| legend=dict( | |
| bgcolor="rgba(255,255,255,0.92)", | |
| bordercolor="#E7E5E4", borderwidth=1, | |
| font=dict(size=10), | |
| ), | |
| xaxis=dict( | |
| gridcolor="#F5F5F4", linecolor="#E7E5E4", | |
| tickfont=dict(color="#78716C", size=10), | |
| title_font=dict(color="#57534E", size=11), | |
| zerolinecolor="#E7E5E4", | |
| ), | |
| yaxis=dict( | |
| gridcolor="#F5F5F4", linecolor="#E7E5E4", | |
| tickfont=dict(color="#78716C", size=10), | |
| title_font=dict(color="#57534E", size=11), | |
| zerolinecolor="#E7E5E4", | |
| ), | |
| ) | |
| # ββ Colour tokens βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SAFFRON = "#FB923C" # primary accent | |
| SAFFRON_D = "#EA580C" # darker saffron | |
| SLATE = "#1C1917" # near-black | |
| STONE = "#78716C" # muted label | |
| BORDER = "#E7E5E4" | |
| BG = "#FAF9F7" | |
| WHITE = "#FFFFFF" | |
| GREEN = "#16A34A" | |
| RED = "#DC2626" | |
| AMBER = "#D97706" | |
| BLUE = "#2563EB" | |
| # ββ Saffron scale for choropleth / sequential maps βββββββββββββββββββββββββββ | |
| SAFFRON_SCALE = [ | |
| [0.0, "#FFF7ED"], | |
| [0.25, "#FED7AA"], | |
| [0.5, "#FB923C"], | |
| [0.75, "#EA580C"], | |
| [1.0, "#7C2D12"], | |
| ] | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def inject_theme(): | |
| import streamlit as st | |
| st.markdown(THEME_CSS, unsafe_allow_html=True) | |
| def page_header(eyebrow: str, title: str, subtitle: str = ""): | |
| import streamlit as st | |
| sub_html = ( | |
| f'<p style="font-family:\'Source Serif 4\',serif; font-size:0.92rem; ' | |
| f'color:#78716C; margin:6px 0 0 0; line-height:1.5;">{subtitle}</p>' | |
| if subtitle else "" | |
| ) | |
| st.markdown(f""" | |
| <div style="margin-bottom:1.75rem; padding-bottom:1.25rem; border-bottom:2px solid #E7E5E4;"> | |
| <p style="font-family:'DM Mono',monospace; font-size:0.58rem; letter-spacing:3.5px; | |
| text-transform:uppercase; color:#FB923C; margin:0 0 7px 0;">{eyebrow}</p> | |
| <h1 style="font-family:'Fraunces',serif; font-size:2.1rem; font-weight:600; | |
| color:#1C1917; margin:0; line-height:1.15;">{title}</h1> | |
| {sub_html} | |
| </div>""", unsafe_allow_html=True) | |
| def section_label(text: str): | |
| import streamlit as st | |
| st.markdown( | |
| f'<p style="font-family:\'DM Mono\',monospace; font-size:0.58rem; ' | |
| f'letter-spacing:3px; text-transform:uppercase; color:#A8A29E; ' | |
| f'margin:0 0 10px 0; padding-bottom:8px; border-bottom:1px solid #F5F5F4;">' | |
| f'{text}</p>', | |
| unsafe_allow_html=True, | |
| ) | |
| def kpi_html(value: str, label: str, color: str = "#1C1917", note: str = "") -> str: | |
| note_html = ( | |
| f'<p style="font-family:\'DM Mono\',monospace; font-size:0.62rem; ' | |
| f'color:#A8A29E; margin:3px 0 0 0;">{note}</p>' | |
| if note else "" | |
| ) | |
| return f""" | |
| <div style="background:#FFFFFF; border:1px solid #E7E5E4; border-radius:8px; padding:1rem 1.25rem;"> | |
| <p style="font-family:'DM Mono',monospace; font-size:0.58rem; letter-spacing:2.5px; | |
| text-transform:uppercase; color:#A8A29E; margin:0 0 5px 0;">{label}</p> | |
| <p style="font-family:'Fraunces',serif; font-size:1.9rem; font-weight:600; | |
| color:{color}; line-height:1; margin:0;">{value}</p> | |
| {note_html} | |
| </div>""" | |
| def signal_card_html(value: str, title: str, body: str, accent: str = "#FB923C") -> str: | |
| return f""" | |
| <div style="background:#FFFFFF; border:1px solid #E7E5E4; border-left:3px solid {accent}; | |
| border-radius:8px; padding:0.85rem 1rem; margin-bottom:7px; | |
| display:flex; align-items:center; gap:0.9rem;"> | |
| <span style="font-family:'Fraunces',serif; font-size:1.55rem; font-weight:600; | |
| color:{accent}; min-width:56px; text-align:right; flex-shrink:0;">{value}</span> | |
| <div> | |
| <p style="font-family:'DM Mono',monospace; font-size:0.6rem; letter-spacing:1.2px; | |
| text-transform:uppercase; color:#57534E; margin:0 0 2px 0;">{title}</p> | |
| <p style="font-family:'Source Serif 4',serif; font-size:0.78rem; | |
| color:#A8A29E; margin:0; line-height:1.4;">{body}</p> | |
| </div> | |
| </div>""" | |
| # NOTE: inject_theme() is now a no-op for page files. | |
| # All CSS is injected once in app.py before st.navigation() runs, | |
| # which means it persists across every page automatically. | |
| def inject_theme(): | |
| pass # CSS already injected globally by app.py | |