Spaces:
Running
Running
Download labs_theme.py from DatologyAI/SafeTulu-Space: direct link, hf CLI and curl.
- Browser
- Download file 24.4 kB
-
https://huggingface.co/spaces/DatologyAI/SafeTulu-Space/resolve/main/labs_theme.py
- Command line
-
hf download hf://spaces/DatologyAI/SafeTulu-Space/labs_theme.py
-
curl -L -o labs_theme.py https://huggingface.co/spaces/DatologyAI/SafeTulu-Space/resolve/main/labs_theme.py
24.4 kB
| """Datology Labs look for the Space, with a light theme (default) and a dark theme. | |
| Colors were sampled from the Datology Labs brand film. The light theme follows the film's opening | |
| scenes (off-white paper, soft blue glows, navy type); the dark theme follows its night scenes | |
| (deep navy, cobalt and teal glows). Both share the cobalt-to-cyan hero, the orange accent and | |
| the film grain. A light-bulb button beside the hero switches between them and remembers the choice. | |
| Everything visual lives here, so app.py stays the same as the plain version apart from a few imports. | |
| """ | |
| from __future__ import annotations | |
| import inspect | |
| import gradio as gr | |
| # --------------------------------------------------------------------------- | |
| # Palette (sampled from the brand film) | |
| # --------------------------------------------------------------------------- | |
| # Shared brand colors | |
| DEEP_COBALT = "#052d78" | |
| COBALT = "#1574eb" | |
| ELECTRIC = "#2f8ff0" # primary blue; "kept" in charts | |
| CYAN = "#39b0df" | |
| ICE = "#a4d9e8" | |
| ORANGE = "#e8692a" # the single warm accent; "removed" in charts | |
| ORANGE_HI = "#f2894c" | |
| EMBER = "#e5b06d" | |
| # Dark theme (the night scenes) | |
| NIGHT = "#040b18" # deepest background | |
| NAVY = "#0a1630" # panels / chart surface | |
| NAVY_2 = "#0c2447" # raised surfaces, inputs | |
| TEXT = "#f2f6fc" | |
| TEXT_MUTED = "#9fb3cf" | |
| LINE = "rgba(140, 180, 235, 0.16)" | |
| LINE_STRONG = "rgba(140, 180, 235, 0.32)" | |
| # Light theme (the opening scenes) | |
| PAPER = "#f6f9fd" # page | |
| WHITE = "#ffffff" # panels / chart surface | |
| PAPER_2 = "#eef3fa" # raised surfaces, inputs | |
| INK_NAVY = "#0b1b3a" # body text (navy captions in the film) | |
| INK_MUTED = "#4b5e7c" | |
| COBALT_TEXT = "#1257c4" # blue that reads as text on white | |
| ORANGE_TEXT = "#c9531a" # orange that reads as text on white | |
| LINE_L = "rgba(21, 60, 120, 0.12)" | |
| LINE_STRONG_L = "rgba(21, 60, 120, 0.24)" | |
| FONT = "Hanken Grotesk" | |
| FONT_MONO = "JetBrains Mono" | |
| # Chart roles. The same two colors pass the palette checks on navy and on white | |
| # (CVD ΔE 27, contrast ≥ 3:1), so bars keep their colors in both themes. | |
| # Axis text, gridlines and the cutoff line switch with the theme via CSS (see PLOT CSS below). | |
| KEPT = ELECTRIC | |
| REMOVED = ORANGE | |
| INK = TEXT_MUTED | |
| GRID = "rgba(159, 179, 207, 0.12)" | |
| CUTOFF_LINE = "rgba(242, 246, 252, 0.7)" | |
| # --------------------------------------------------------------------------- | |
| # Gradio theme: light values for the default, dark values under .dark | |
| # --------------------------------------------------------------------------- | |
| _SHARED = dict( | |
| body_text_weight="400", | |
| border_color_accent=ELECTRIC, | |
| color_accent=ELECTRIC, | |
| shadow_drop="none", | |
| shadow_spread="0px", | |
| block_border_width="1px", | |
| block_radius="16px", | |
| container_radius="16px", | |
| block_label_background_fill="transparent", | |
| block_label_border_width="0px", | |
| block_label_text_weight="500", | |
| block_title_background_fill="transparent", | |
| block_title_border_width="0px", | |
| block_title_text_weight="500", | |
| block_title_padding="0", | |
| panel_background_fill="transparent", | |
| input_border_color_focus=ELECTRIC, | |
| input_border_width="1px", | |
| checkbox_background_color_selected=ELECTRIC, | |
| checkbox_border_color_selected=ELECTRIC, | |
| checkbox_border_color_focus=ELECTRIC, | |
| checkbox_label_border_width="1px", | |
| checkbox_label_shadow="none", | |
| checkbox_label_text_weight="400", | |
| slider_color=ORANGE, | |
| loader_color=ORANGE, | |
| stat_background_fill=f"linear-gradient(90deg, {COBALT}, {CYAN})", | |
| button_primary_background_fill=ORANGE, | |
| button_primary_background_fill_hover=ORANGE_HI, | |
| button_primary_border_color=ORANGE, | |
| button_primary_border_color_hover=ORANGE_HI, | |
| button_primary_text_color="#ffffff", | |
| button_primary_text_color_hover="#ffffff", | |
| button_secondary_border_color_hover=ELECTRIC, | |
| button_secondary_shadow="none", | |
| ) | |
| _DARK = dict( | |
| body_background_fill=NIGHT, | |
| body_text_color=TEXT, | |
| body_text_color_subdued=TEXT_MUTED, | |
| background_fill_primary=NAVY, | |
| background_fill_secondary=NAVY_2, | |
| border_color_primary=LINE, | |
| border_color_accent_subdued=LINE_STRONG, | |
| color_accent_soft="rgba(47, 143, 240, 0.18)", | |
| link_text_color=CYAN, | |
| link_text_color_hover=ICE, | |
| link_text_color_visited=CYAN, | |
| link_text_color_active=ICE, | |
| code_background_fill=NIGHT, | |
| shadow_drop_lg="0 20px 60px rgba(0, 0, 0, 0.35)", | |
| block_background_fill="rgba(10, 22, 48, 0.72)", | |
| block_border_color=LINE, | |
| block_label_text_color=TEXT_MUTED, | |
| block_title_text_color=TEXT, | |
| input_background_fill=NAVY_2, | |
| input_background_fill_focus=NAVY_2, | |
| input_background_fill_hover=NAVY_2, | |
| input_border_color=LINE, | |
| input_border_color_hover=LINE_STRONG, | |
| checkbox_background_color=NIGHT, | |
| checkbox_background_color_hover=NIGHT, | |
| checkbox_background_color_focus=NIGHT, | |
| checkbox_border_color=LINE_STRONG, | |
| checkbox_label_background_fill="rgba(12, 36, 71, 0.6)", | |
| checkbox_label_background_fill_hover="rgba(21, 116, 235, 0.14)", | |
| checkbox_label_background_fill_selected="rgba(21, 116, 235, 0.22)", | |
| checkbox_label_border_color=LINE, | |
| checkbox_label_border_color_hover=LINE_STRONG, | |
| checkbox_label_border_color_selected="rgba(47, 143, 240, 0.65)", | |
| checkbox_label_text_color=TEXT, | |
| checkbox_label_text_color_selected=TEXT, | |
| table_border_color=LINE, | |
| table_even_background_fill=NAVY, | |
| table_odd_background_fill=NAVY_2, | |
| table_row_focus="rgba(21, 116, 235, 0.18)", | |
| table_text_color=TEXT, | |
| button_primary_shadow="0 0 24px rgba(232, 105, 42, 0.35)", | |
| button_primary_shadow_hover="0 0 32px rgba(232, 105, 42, 0.5)", | |
| button_secondary_background_fill="rgba(12, 36, 71, 0.6)", | |
| button_secondary_background_fill_hover="rgba(21, 116, 235, 0.18)", | |
| button_secondary_border_color=LINE_STRONG, | |
| button_secondary_text_color=TEXT, | |
| button_secondary_text_color_hover=TEXT, | |
| ) | |
| _LIGHT = dict( | |
| body_background_fill=PAPER, | |
| body_text_color=INK_NAVY, | |
| body_text_color_subdued=INK_MUTED, | |
| background_fill_primary=WHITE, | |
| background_fill_secondary=PAPER_2, | |
| border_color_primary=LINE_L, | |
| border_color_accent_subdued=LINE_STRONG_L, | |
| color_accent_soft="rgba(21, 116, 235, 0.10)", | |
| link_text_color=COBALT_TEXT, | |
| link_text_color_hover=COBALT, | |
| link_text_color_visited=COBALT_TEXT, | |
| link_text_color_active=COBALT, | |
| code_background_fill=PAPER, | |
| shadow_drop_lg="0 18px 50px rgba(21, 60, 120, 0.10)", | |
| block_background_fill="rgba(255, 255, 255, 0.82)", | |
| block_border_color=LINE_L, | |
| block_label_text_color=INK_MUTED, | |
| block_title_text_color=INK_NAVY, | |
| input_background_fill=WHITE, | |
| input_background_fill_focus=WHITE, | |
| input_background_fill_hover=WHITE, | |
| input_border_color=LINE_STRONG_L, | |
| input_border_color_hover="rgba(21, 60, 120, 0.36)", | |
| checkbox_background_color=WHITE, | |
| checkbox_background_color_hover=WHITE, | |
| checkbox_background_color_focus=WHITE, | |
| checkbox_border_color="rgba(21, 60, 120, 0.36)", | |
| checkbox_label_background_fill=PAPER_2, | |
| checkbox_label_background_fill_hover="rgba(21, 116, 235, 0.08)", | |
| checkbox_label_background_fill_selected="rgba(21, 116, 235, 0.10)", | |
| checkbox_label_border_color=LINE_L, | |
| checkbox_label_border_color_hover=LINE_STRONG_L, | |
| checkbox_label_border_color_selected="rgba(21, 116, 235, 0.45)", | |
| checkbox_label_text_color=INK_NAVY, | |
| checkbox_label_text_color_selected=INK_NAVY, | |
| table_border_color=LINE_L, | |
| table_even_background_fill=WHITE, | |
| table_odd_background_fill=PAPER, | |
| table_row_focus="rgba(21, 116, 235, 0.08)", | |
| table_text_color=INK_NAVY, | |
| button_primary_shadow="0 6px 18px rgba(232, 105, 42, 0.25)", | |
| button_primary_shadow_hover="0 8px 24px rgba(232, 105, 42, 0.35)", | |
| button_secondary_background_fill=WHITE, | |
| button_secondary_background_fill_hover="rgba(21, 116, 235, 0.06)", | |
| button_secondary_border_color=LINE_STRONG_L, | |
| button_secondary_text_color=INK_NAVY, | |
| button_secondary_text_color_hover=INK_NAVY, | |
| ) | |
| def build_theme() -> gr.themes.Base: | |
| theme = gr.themes.Base( | |
| primary_hue=gr.themes.colors.blue, | |
| secondary_hue=gr.themes.colors.orange, | |
| neutral_hue=gr.themes.colors.slate, | |
| radius_size=gr.themes.sizes.radius_lg, | |
| font=[gr.themes.GoogleFont(FONT, weights=(300, 400, 500, 600)), "ui-sans-serif", "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont(FONT_MONO), "ui-monospace", "monospace"], | |
| ) | |
| valid = set(inspect.signature(theme.set).parameters) | |
| values = {} | |
| for k, v in _SHARED.items(): | |
| if k in valid: | |
| values[k] = v | |
| if f"{k}_dark" in valid: | |
| values[f"{k}_dark"] = v | |
| for k, v in _LIGHT.items(): | |
| if k in valid: | |
| values[k] = v | |
| for k, v in _DARK.items(): | |
| if f"{k}_dark" in valid: | |
| values[f"{k}_dark"] = v | |
| elif k in valid and k not in _LIGHT: | |
| values[k] = v | |
| return theme.set(**values) | |
| # Opens in light unless the viewer chose dark before (remembered in this browser) or the URL has | |
| # ?__theme=dark. Gradio would otherwise follow the system setting, so the class is set explicitly. | |
| # The light-bulb button toggles the theme; a click handler on the document survives re-renders. | |
| THEME_JS = """ | |
| () => { | |
| const KEY = 'safetulu-theme'; | |
| const sync = (dark) => { | |
| const btn = document.getElementById('theme-toggle'); | |
| if (!btn) return; | |
| const label = dark ? 'Switch to light theme' : 'Switch to dark theme'; | |
| btn.setAttribute('aria-label', label); | |
| btn.setAttribute('title', label); | |
| btn.setAttribute('aria-pressed', String(dark)); | |
| }; | |
| const apply = (mode) => { | |
| const dark = mode === 'dark'; | |
| document.body.classList.toggle('dark', dark); | |
| document.documentElement.style.colorScheme = dark ? 'dark' : 'light'; | |
| sync(dark); | |
| }; | |
| let saved = null; | |
| try { saved = localStorage.getItem(KEY); } catch (e) {} | |
| const param = new URLSearchParams(window.location.search).get('__theme'); | |
| apply(param === 'dark' || param === 'light' ? param : (saved === 'dark' ? 'dark' : 'light')); | |
| document.addEventListener('click', (event) => { | |
| if (!event.target.closest('#theme-toggle')) return; | |
| const next = document.body.classList.contains('dark') ? 'light' : 'dark'; | |
| apply(next); | |
| try { localStorage.setItem(KEY, next); } catch (e) {} | |
| }); | |
| // "Use via API" lives in the footer; hide it there and open it from the hero button instead. | |
| const apiBtn = () => Array.from(document.querySelectorAll('footer button, footer a')) | |
| .find((el) => /use via api/i.test(el.textContent || '')); | |
| const hideFooterApi = () => { | |
| const el = apiBtn(); | |
| if (!el) return; | |
| el.style.display = 'none'; | |
| const sep = el.nextElementSibling; | |
| if (sep && /^\\s*[·•]\\s*$/.test(sep.textContent || '')) sep.style.display = 'none'; | |
| }; | |
| hideFooterApi(); | |
| new MutationObserver(hideFooterApi).observe(document.body, { childList: true, subtree: true }); | |
| document.addEventListener('click', (event) => { | |
| if (!event.target.closest('.api-docs-btn')) return; | |
| event.preventDefault(); | |
| const el = apiBtn(); | |
| if (el) el.click(); | |
| }); | |
| // The hero (and its button) can render after this runs; label it once it exists. | |
| const obs = new MutationObserver(() => { | |
| if (document.getElementById('theme-toggle')) { sync(document.body.classList.contains('dark')); obs.disconnect(); } | |
| }); | |
| obs.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| """ | |
| # --------------------------------------------------------------------------- | |
| # Brand pieces | |
| # --------------------------------------------------------------------------- | |
| def _grain(rgb: str) -> str: | |
| return ( | |
| "url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'>" | |
| "<filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/>" | |
| f"<feColorMatrix values='0 0 0 0 {rgb} 0 0 0 0 {rgb} 0 0 0 0 {rgb} 0 0 0 0.6 0'/></filter>" | |
| "<rect width='100%' height='100%' filter='url(%23n)'/></svg>\")" | |
| ) | |
| _GRAIN = _grain("1") # light speckle, for dark surfaces and the hero | |
| _GRAIN_DARK = _grain("0.1") # dark speckle, for the light page | |
| # Light-bulb icon (Lucide "lightbulb", ISC license). The glass fills in when the light theme is on. | |
| _BULB = ( | |
| '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" ' | |
| 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' | |
| '<path class="bulb-glass" d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 ' | |
| '1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>' | |
| ) | |
| def hero_html(repo_id: str, n_examples: int, official_pct: float) -> str: | |
| return f""" | |
| <div class="hero-wrap"> | |
| <section class="hero"> | |
| <div class="hero-glow"></div> | |
| <div class="hero-inner"> | |
| <h1><b>Build your own</b> SafeTulu</h1> | |
| <p class="hero-sub">remove the Tulu 3 data that sits closest to harm, at any threshold you choose</p> | |
| </div> | |
| </section> | |
| <button id="theme-toggle" class="theme-toggle" type="button" aria-pressed="false" | |
| aria-label="Switch to dark theme" title="Switch to dark theme">{_BULB}</button> | |
| </div> | |
| <div class="hero-meta"> | |
| <p>Every Tulu 3 example is scored by how close its training gradient is to harmful SafeRLHF data. | |
| Pick a threshold and the sources to filter, see what gets removed, then download the IDs, | |
| copy a script that reproduces your selection, or push the result to your own Hub account.</p> | |
| <div class="hero-actions"> | |
| <a class="hero-btn hero-btn-dataset" href="https://huggingface.co/datasets/DatologyAI/SafeTulu" target="_blank" rel="noopener">🤗 Dataset</a> | |
| <a class="hero-btn api-docs-btn" href="#" role="button">API Docs for Your AI Agent</a> | |
| </div> | |
| </div> | |
| """ | |
| CSS = f""" | |
| /* ---- theme tokens: light by default, dark under .dark ---- */ | |
| :root {{ | |
| --lt-page: {PAPER}; | |
| --lt-glow-1: rgba(21,116,235,0.10); | |
| --lt-glow-2: rgba(57,176,223,0.14); | |
| --lt-glow-3: rgba(232,105,42,0.06); | |
| --lt-grain: {_GRAIN_DARK}; | |
| --lt-grain-opacity: 0.05; | |
| --lt-grain-blend: multiply; | |
| --lt-ink: {INK_NAVY}; | |
| --lt-ink-muted: {INK_MUTED}; | |
| --lt-line: {LINE_L}; | |
| --lt-label: {COBALT_TEXT}; | |
| --lt-edge: rgba(21,116,235,0.45); | |
| --lt-tile-bg: linear-gradient(160deg, rgba(21,116,235,0.08), rgba(255,255,255,0.92) 55%); | |
| --lt-tile-shadow: 0 10px 30px rgba(21,60,120,0.06); | |
| --lt-accent-num: {ORANGE_TEXT}; | |
| --lt-meta-em: {ORANGE_TEXT}; | |
| --lt-link: {COBALT_TEXT}; | |
| --lt-link-line: rgba(18,87,196,0.35); | |
| --lt-tab-active: {INK_NAVY}; | |
| --lt-plot-ink: {INK_MUTED}; | |
| --lt-plot-strong: {INK_NAVY}; | |
| --lt-plot-grid: rgba(21,60,120,0.10); | |
| --lt-plot-cutoff: rgba(11,27,58,0.6); | |
| --lt-hero-shadow: 0 24px 60px rgba(21,116,235,0.22); | |
| }} | |
| .dark {{ | |
| --lt-page: {NIGHT}; | |
| --lt-glow-1: rgba(21,116,235,0.30); | |
| --lt-glow-2: rgba(6,78,111,0.45); | |
| --lt-glow-3: rgba(5,45,120,0.55); | |
| --lt-grain: {_GRAIN}; | |
| --lt-grain-opacity: 0.07; | |
| --lt-grain-blend: soft-light; | |
| --lt-ink: {TEXT}; | |
| --lt-ink-muted: {TEXT_MUTED}; | |
| --lt-line: {LINE}; | |
| --lt-label: {CYAN}; | |
| --lt-edge: rgba(57,176,223,0.7); | |
| --lt-tile-bg: linear-gradient(160deg, rgba(21,116,235,0.16), rgba(10,22,48,0.6) 55%); | |
| --lt-tile-shadow: none; | |
| --lt-accent-num: {ORANGE_HI}; | |
| --lt-meta-em: {EMBER}; | |
| --lt-link: {CYAN}; | |
| --lt-link-line: rgba(57,176,223,0.4); | |
| --lt-tab-active: {TEXT}; | |
| --lt-plot-ink: {TEXT_MUTED}; | |
| --lt-plot-strong: {TEXT}; | |
| --lt-plot-grid: rgba(159,179,207,0.12); | |
| --lt-plot-cutoff: rgba(242,246,252,0.7); | |
| --lt-hero-shadow: none; | |
| }} | |
| /* page: soft glows and film grain over the theme's base color */ | |
| body, gradio-app {{ | |
| background: radial-gradient(1100px 620px at 8% -8%, var(--lt-glow-1), transparent 60%), | |
| radial-gradient(900px 700px at 105% 20%, var(--lt-glow-2), transparent 60%), | |
| radial-gradient(700px 500px at 60% 115%, var(--lt-glow-3), transparent 65%), | |
| var(--lt-page) !important; | |
| transition: background-color 0.25s ease; | |
| }} | |
| body::after {{ | |
| content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 9999; | |
| background-image: var(--lt-grain); opacity: var(--lt-grain-opacity); mix-blend-mode: var(--lt-grain-blend); | |
| }} | |
| .gradio-container {{ width: 100% !important; max-width: 1240px !important; margin: 0 auto !important; background: transparent !important; }} | |
| footer {{ opacity: 0.55; }} | |
| /* hero: the closing card of the brand film (same in both themes) */ | |
| .hero {{ | |
| position: relative; overflow: hidden; border-radius: 24px; min-height: 280px; | |
| display: flex; align-items: center; justify-content: center; text-align: center; | |
| background: | |
| radial-gradient(520px 340px at 16% 18%, rgba(164,217,232,0.95), transparent 70%), | |
| linear-gradient(118deg, {ICE} 0%, {CYAN} 26%, #1a9be8 48%, {COBALT} 76%, #0b45cf 100%); | |
| box-shadow: var(--lt-hero-shadow); | |
| isolation: isolate; | |
| }} | |
| .hero-glow {{ | |
| position: absolute; width: 420px; height: 420px; right: 14%; top: -6%; z-index: -1; | |
| background: radial-gradient(circle at 45% 50%, rgba(242,137,76,0.95), rgba(232,105,42,0.55) 38%, | |
| rgba(196,120,200,0.25) 58%, transparent 72%); | |
| filter: blur(30px); | |
| }} | |
| .hero::after {{ | |
| content: ""; position: absolute; inset: 0; pointer-events: none; | |
| background-image: {_GRAIN}; opacity: 0.22; mix-blend-mode: overlay; | |
| }} | |
| @media (max-width: 640px) {{ | |
| .hero-glow {{ width: 260px; height: 260px; right: -18%; top: -14%; }} | |
| .hero {{ border-radius: 20px; }} | |
| }} | |
| .hero-inner {{ position: relative; padding: 44px 24px; color: #fff !important; }} | |
| .hero h1 {{ | |
| margin: 0; color: #fff !important; font-weight: 300; letter-spacing: -0.035em; line-height: 1.02; | |
| font-size: clamp(38px, 6.4vw, 76px); text-shadow: 0 2px 24px rgba(5,45,120,0.25); | |
| }} | |
| .hero h1 b, .hero h1 strong {{ font-weight: 500 !important; color: #fff !important; }} | |
| .hero p.hero-sub, .hero .hero-sub {{ | |
| margin: 14px 0 0 !important; font-size: clamp(17px, 2vw, 22px) !important; font-weight: 300 !important; color: #fff !important; | |
| text-shadow: 0 1px 14px rgba(5,45,120,0.35); | |
| }} | |
| /* light-bulb theme switch: just outside the title box, aligned with its top right corner. | |
| The wrapper leaves an equal gutter on both sides, so the box stays centered. | |
| Everything is !important because Gradio's own button and text styles can otherwise win. */ | |
| .hero-wrap {{ position: relative !important; padding: 0 60px !important; }} | |
| #theme-toggle.theme-toggle {{ | |
| position: absolute !important; top: 0 !important; right: 0 !important; z-index: 2 !important; | |
| width: 44px !important; height: 44px !important; min-width: 44px !important; min-height: 44px !important; | |
| margin: 0 !important; padding: 0 !important; line-height: 0 !important; font-size: 0 !important; | |
| display: flex !important; align-items: center !important; justify-content: center !important; | |
| border-radius: 12px !important; cursor: pointer !important; | |
| background: #ffffff !important; border: 1px solid {LINE_STRONG_L} !important; color: {INK_NAVY} !important; | |
| box-shadow: 0 6px 18px rgba(21,60,120,0.10), 0 0 0 4px rgba(255,190,110,0.0) !important; | |
| transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease !important; | |
| }} | |
| #theme-toggle.theme-toggle svg {{ display: block !important; width: 20px !important; height: 20px !important; margin: 0 !important; flex: none !important; }} | |
| #theme-toggle.theme-toggle:hover {{ border-color: {COBALT} !important; transform: translateY(-1px); box-shadow: 0 8px 22px rgba(21,60,120,0.14), 0 0 18px rgba(255,190,110,0.45) !important; }} | |
| #theme-toggle.theme-toggle:focus-visible {{ outline: 2px solid {COBALT} !important; outline-offset: 3px !important; }} | |
| #theme-toggle.theme-toggle .bulb-glass {{ fill: #ffc970 !important; transition: fill 0.2s ease; }} | |
| .dark #theme-toggle.theme-toggle {{ | |
| background: {NAVY_2} !important; border-color: {LINE_STRONG} !important; color: {TEXT} !important; | |
| box-shadow: none !important; | |
| }} | |
| .dark #theme-toggle.theme-toggle:hover {{ border-color: {ELECTRIC} !important; }} | |
| .dark #theme-toggle.theme-toggle .bulb-glass {{ fill: none !important; }} | |
| /* phones: no room beside the box, so the bulb sits inside its top right corner */ | |
| @media (max-width: 700px) {{ | |
| .hero-wrap {{ padding: 0 !important; }} | |
| #theme-toggle.theme-toggle {{ | |
| top: 12px !important; right: 12px !important; color: #fff !important; | |
| background: rgba(255,255,255,0.18) !important; border-color: rgba(255,255,255,0.5) !important; | |
| backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); | |
| box-shadow: 0 0 18px rgba(255,190,110,0.45) !important; | |
| }} | |
| .dark #theme-toggle.theme-toggle {{ background: rgba(4,11,24,0.35) !important; border-color: rgba(255,255,255,0.3) !important; box-shadow: none !important; color: #fff !important; }} | |
| .hero-inner {{ padding-top: 68px !important; }} | |
| }} | |
| .hero-meta {{ max-width: 860px; margin: 22px auto 6px; text-align: center; color: var(--lt-ink-muted); }} | |
| .hero-meta p {{ margin: 0 0 8px; font-size: 15px; line-height: 1.6; color: var(--lt-ink-muted) !important; }} | |
| .hero-meta .meta-line {{ font-size: 13px; letter-spacing: 0.01em; }} | |
| .hero-meta .meta-line span {{ margin: 0 6px; opacity: 0.6; }} | |
| .hero-meta em {{ font-style: normal; color: var(--lt-meta-em) !important; }} | |
| .hero-actions {{ display: flex; justify-content: center; gap: 14px; flex-wrap: wrap; margin: 18px 0 6px; }} | |
| .hero-meta a.hero-btn, .hero-actions .hero-btn {{ | |
| display: inline-flex !important; align-items: center; justify-content: center; gap: 8px; | |
| padding: 12px 26px !important; border-radius: 999px !important; font-size: 16px !important; font-weight: 600 !important; | |
| cursor: pointer; color: #fff !important; border: none !important; border-bottom: none !important; | |
| text-decoration: none !important; line-height: 1.2 !important; | |
| background: linear-gradient(118deg, {CYAN} 0%, {COBALT} 100%) !important; | |
| box-shadow: 0 8px 22px rgba(21,116,235,0.28); transition: transform 0.15s ease, box-shadow 0.15s ease; | |
| }} | |
| .hero-meta a.hero-btn:hover, .hero-meta a.hero-btn:focus, .hero-meta a.hero-btn:visited {{ | |
| color: #fff !important; text-decoration: none !important; border-bottom: none !important; | |
| }} | |
| .hero-meta a.hero-btn:hover {{ transform: translateY(-1px); box-shadow: 0 12px 28px rgba(21,116,235,0.36); }} | |
| .hero-meta a {{ color: var(--lt-link) !important; text-decoration: none; border-bottom: 1px solid var(--lt-link-line); }} | |
| /* stat tiles */ | |
| .tiles {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }} | |
| .tile {{ | |
| position: relative; overflow: hidden; border: 1px solid var(--lt-line); border-radius: 16px; padding: 16px 18px; | |
| background: var(--lt-tile-bg); box-shadow: var(--lt-tile-shadow); | |
| }} | |
| .tile::before {{ | |
| content: ""; position: absolute; left: 18px; right: 18px; top: 0; height: 1px; | |
| background: linear-gradient(90deg, transparent, var(--lt-edge), transparent); | |
| }} | |
| .tile-label {{ font-size: 11px; font-weight: 500; color: var(--lt-label); text-transform: uppercase; letter-spacing: 0.12em; }} | |
| .tile-value {{ font-size: 30px; font-weight: 400; letter-spacing: -0.02em; color: var(--lt-ink); | |
| font-variant-numeric: tabular-nums; margin-top: 6px; }} | |
| .tile-sub {{ font-size: 12px; color: var(--lt-ink-muted); margin-top: 4px; line-height: 1.4; }} | |
| .tile.accent .tile-value {{ color: var(--lt-accent-num); }} | |
| /* section headings above the charts */ | |
| .section-title p {{ margin: 6px 0 -4px !important; font-size: 13px !important; font-weight: 500 !important; | |
| color: var(--lt-ink-muted) !important; text-transform: uppercase; letter-spacing: 0.12em; }} | |
| /* tabs */ | |
| button[role="tab"] {{ color: var(--lt-ink-muted) !important; font-weight: 500; }} | |
| button[role="tab"][aria-selected="true"] {{ color: var(--lt-tab-active) !important; border-color: {ORANGE} !important; }} | |
| /* PLOT CSS: axis text, gridlines and the cutoff line follow the theme without re-rendering the charts. | |
| Hover tooltips stay navy in both themes. */ | |
| .js-plotly-plot .cartesianlayer text, | |
| .js-plotly-plot .infolayer text {{ fill: var(--lt-plot-ink) !important; }} | |
| .js-plotly-plot .infolayer .annotation text {{ fill: var(--lt-plot-strong) !important; }} | |
| .js-plotly-plot .gridlayer path, | |
| .js-plotly-plot path.xlines-above, | |
| .js-plotly-plot path.ylines-above {{ stroke: var(--lt-plot-grid) !important; }} | |
| .js-plotly-plot .shapelayer path {{ stroke: var(--lt-plot-cutoff) !important; }} | |
| """ | |