roamify / src /styles /dark_theme.py
jofaichow's picture
v0.1.21 — Remove hermes docs from repo, dark theme fix, Tokyo default
0c740f0
"""Dark theme based on Bootswatch Cyborg ('Jet black and electric blue')."""
DARK_THEME_CSS = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600;700&display=swap');
/* ── Cyborg palette ── */
:root {
--bg-primary: #060606;
--bg-card: #111111;
--bg-card-open: #1a1a1a;
--accent: #2a9fd6;
--accent-hover: #1a7099;
--text-primary: #dee2e6;
--text-muted: #adafae;
--heading: #ffffff;
--border: #222222;
}
/* ── Global ── */
html, body, [class*="css"] {
font-family: 'Inter', 'IBMPlexMono', sans-serif;
font-size: 20px !important;
}
/* ── Main background ── */
.stApp {
background-color: var(--bg-primary) !important;
color: #ffffff !important;
}
/* ── Hide the top header bar ── */
header[data-testid="stHeader"] {
display: none !important;
}
[data-testid="stToolbar"] {
display: none !important;
}
[data-testid="stAppDeployButton"] {
display: none !important;
}
/* Hide heading anchor links that appear on hover */
h1 a, h2 a, h3 a, h1 a:link, h2 a:link, h3 a:link,
h1 a:visited, h2 a:visited, h3 a:visited {
display: none !important;
}
.headerlink {
display: none !important;
}
/* ── Headings ── */
h1, h2, h3 {
color: var(--accent) !important;
font-weight: 700 !important;
}
h1 { font-size: 2.4rem !important; }
h2 { font-size: 1.8rem !important; }
h3 { font-size: 1.4rem !important; }
/* ── Expander cards ── */
.stExpander {
background-color: var(--bg-card) !important;
border: 1px solid var(--border) !important;
border-radius: 10px !important;
margin-bottom: 8px !important;
}
.stExpander details:not([open]) {
min-height: 82px !important;
}
.stExpander:hover {
border-color: var(--accent) !important;
}
.stExpander:hover details[open] {
border-color: var(--accent) !important;
}
.stExpander details[open] {
background-color: var(--bg-card-open) !important;
border-color: var(--border) !important;
}
.stExpander summary {
font-size: 20px !important;
color: var(--accent) !important;
font-weight: 700 !important;
line-height: 1.5 !important;
background-color: var(--bg-card) !important;
}
.stExpander summary strong {
color: var(--accent) !important;
font-weight: 700 !important;
font-size: 22px !important;
display: block !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.stExpander summary p {
font-size: 16px !important;
color: var(--text-muted) !important;
}
.stExpander summary:hover {
color: var(--accent-hover) !important;
background-color: var(--bg-card) !important;
}
/* ── Expanded card content ── */
.stExpander div[data-testid="stExpanderDetails"] {
border-top-color: var(--border) !important;
}
.stExpander div[data-testid="stExpanderDetails"] p,
.stExpander div[data-testid="stExpanderDetails"] span {
font-size: 16px !important;
line-height: 1.5 !important;
}
.stExpander div[data-testid="stExpanderDetails"] em {
font-size: 15px !important;
}
/* ── Uniform card heights via fixed desc area ── */
.card-desc {
font-size: 16px !important;
line-height: 1.5 !important;
min-height: 100px !important;
display: block !important;
margin-bottom: 4px !important;
}
/* ── Input widgets ── */
.stTextInput > div > div > input,
.stDateInput input,
.stNumberInput input,
.stSelectbox div > div > input {
font-size: 18px !important;
background-color: var(--bg-primary) !important;
color: #ffffff !important;
border-color: var(--border) !important;
}
/* All widget labels — force white text */
.stSelectbox label,
.stTextInput label,
.stNumberInput label,
.stDateInput label,
.stRadio label,
label[data-baseweb="label"] {
color: #ffffff !important;
}
/* Selectbox trigger and dropdown — dark theme */
.stSelectbox div[data-baseweb="select"] > div {
background-color: var(--bg-primary) !important;
color: #ffffff !important;
border-color: var(--border) !important;
}
.stSelectbox div[data-baseweb="select"] > div:hover {
border-color: var(--accent) !important;
}
[data-baseweb="popover"] {
background-color: var(--bg-card) !important;
border: 1px solid var(--border) !important;
border-radius: 8px !important;
}
[data-baseweb="popover"] li[role="option"] {
background-color: var(--bg-card) !important;
color: #ffffff !important;
font-size: 16px !important;
}
[data-baseweb="popover"] li[role="option"]:hover {
background-color: var(--bg-card-open) !important;
color: #ffffff !important;
}
[data-baseweb="popover"] li[role="option"][aria-selected="true"] {
background-color: var(--accent) !important;
color: #ffffff !important;
}
/* ── Button ── */
.stButton > button,
button[type="submit"],
button[kind="secondaryFormSubmit"],
button[kind="formSubmit"] {
font-size: 13px !important;
padding: 10px 16px !important;
background-color: var(--accent) !important;
color: #ffffff !important;
border: none !important;
border-radius: 8px !important;
white-space: nowrap !important;
}
.stButton > button:hover,
button[type="submit"]:hover,
button[kind="secondaryFormSubmit"]:hover,
button[kind="formSubmit"]:hover {
background-color: var(--accent-hover) !important;
}
/* ── Spinner ── */
.stSpinner > div {
font-size: 18px !important;
color: var(--accent) !important;
}
/* ── Map: fill full width ── */
.stFolium {
width: 100% !important;
}
.stFolium > div {
width: 100% !important;
}
.stFolium iframe {
width: 100% !important;
}
/* Push map container down to align with cards */
.stCustomComponentV1 {
display: block !important;
}
iframe[title="streamlit_folium.st_folium"] {
display: block !important;
}
/* Hide Leaflet attribution label */
.leaflet-control-attribution {
display: none !important;
}
/* Hide Leaflet zoom controls (+ and -) */
.leaflet-control-zoom {
display: none !important;
}
/* ── Reduce top padding ── */
.block-container {
padding-top: 0 !important;
}
/* Squeeze title closer to top */
.main > div:first-child {
margin-top: -8px !important;
}
h1 {
margin-top: 0 !important;
padding-top: 0 !important;
}
/* ── Hide JS-tool iframes ── */
iframe[title="st.iframe"] {
display: none !important;
}
/* ── Hide scrollbars on all panels (keep scroll functionality) ── */
::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
* {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
/* ── Hide sidebar completely ── */
section[data-testid="stSidebar"] {
display: none !important;
}
section[data-testid="stSidebar"] + div {
margin-left: 0 !important;
}
/* ── Flexible panel heights: dynamically set by JS ── */
/* Fallback height (JS overrides with !important) */
.stVerticalBlock[data-testid="stVerticalBlock"] > [data-testid="stLayoutWrapper"] > .stVerticalBlock {
max-height: 800px;
}
/* ── Map container: cap height to prevent unbounded growth ── */
.stCustomComponentV1 {
height: 800px !important;
max-height: 800px !important;
overflow: hidden !important;
}
.stCustomComponentV1 iframe {
max-height: 800px !important;
}
/* ── Category filter: horizontal radio pills ── */
.stRadio label[data-baseweb="label"] {
font-size: 12px !important;
color: #ffffff !important;
margin-bottom: 0 !important;
}
.stRadio > div[role="radiogroup"] {
flex-direction: row !important;
gap: 8px 3px !important;
flex-wrap: wrap !important;
}
.stRadio > div[role="radiogroup"] > label {
background-color: var(--bg-card) !important;
border: 1px solid var(--border) !important;
border-radius: 20px !important;
padding: 4px 12px !important;
min-height: 42px !important;
line-height: 20px !important;
color: #ffffff !important;
font-size: 16px !important;
font-weight: 500 !important;
cursor: pointer !important;
transition: all 0.15s ease !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
white-space: nowrap !important;
}
/* Pill inner text — emoji + name */
.stRadio > div[role="radiogroup"] > label > div:last-child {
font-size: 13px !important;
line-height: 1 !important;
}
/* Force white text on all inner elements of radio pills */
.stRadio > div[role="radiogroup"] > label * {
color: #ffffff !important;
}
.stRadio > div[role="radiogroup"] > label:hover {
border-color: var(--accent) !important;
color: #ffffff !important;
}
.stRadio > div[role="radiogroup"] > label[data-baseweb="radio"] {
justify-content: center !important;
}
/* Selected / checked pill */
.stRadio > div[role="radiogroup"] > label:has(input:checked),
.stRadio > div[role="radiogroup"] > label[aria-checked="true"] {
background-color: var(--accent) !important;
border-color: var(--accent) !important;
color: #ffffff !important;
font-weight: 700 !important;
}
/* Hide the native radio circle */
.stRadio > div[role="radiogroup"] > label > div:first-child {
display: none !important;
}
.stRadio > div[role="radiogroup"] > label > div:last-child {
margin-left: 0 !important;
padding-left: 0 !important;
}
/* ── Responsive search controls — tight gaps, proportional columns ── */
[data-testid="stHorizontalBlock"] {
flex-wrap: wrap !important;
gap: 0.2rem 15px !important;
}
/* Stack layout: all columns full width, JS handles search controls 4-row layout */
.stack-columns [data-testid="stHorizontalBlock"] > [data-testid="stColumn"] {
flex: 0 1 100% !important;
}
/* Mobile: hide the map entirely */
.stack-columns [data-testid="stHorizontalBlock"] .stCustomComponentV1 {
display: none !important;
}
/* Mobile: hide the '🗺️ Map' title too */
.stack-columns [data-testid="stHorizontalBlock"] [data-testid="stColumn"]:has(.stCustomComponentV1) h3 {
display: none !important;
}
/* Align search/refresh buttons with selectbox dropdown (not label) */
[data-testid="stHorizontalBlock"] > [data-testid="stColumn"]:nth-child(5) > div:first-child,
[data-testid="stHorizontalBlock"] > [data-testid="stColumn"]:nth-child(6) > div:first-child {
padding-top: 30px !important;
}
/* Make all form inputs fill their column width */
.stTextInput,
.stSelectbox,
.stRadio,
.stButton {
width: 100% !important;
}
.stTextInput > div,
.stSelectbox > div,
.stSelectbox > div > div {
width: 100% !important;
}
/* Compact selectbox — shrink height */
.stSelectbox {
padding-top: 0 !important;
margin-top: 0 !important;
}
.stSelectbox > div > div:first-child {
padding: 0 8px !important;
min-height: 38px !important;
}
.stSelectbox div[data-baseweb="select"] {
height: 38px !important;
}
.stSelectbox div[data-baseweb="select"] > div {
min-height: 38px !important;
padding: 0 8px !important;
font-size: 18px !important;
}
/* Dropdown popover menu — dark bg + light text */
[data-baseweb="popover"] {
background-color: var(--bg-card) !important;
border: 1px solid var(--border) !important;
border-radius: 8px !important;
}
[data-baseweb="popover"] li[role="option"] {
background-color: var(--bg-card) !important;
color: #ffffff !important;
font-size: 16px !important;
}
[data-baseweb="popover"] li[role="option"]:hover {
background-color: var(--bg-card-open) !important;
color: #ffffff !important;
}
[data-baseweb="popover"] li[role="option"][aria-selected="true"] {
background-color: var(--accent) !important;
color: #ffffff !important;
}
.stSelectbox label {
font-size: 13px !important;
margin-bottom: 4px !important;
color: #ffffff !important;
}
/* Compact text input to match */
.stTextInput > div > div > input {
min-height: 38px !important;
padding: 0 8px !important;
}
.stTextInput label {
font-size: 13px !important;
margin-bottom: 4px !important;
color: #ffffff !important;
}
/* ── Force dark on all wrappers (iframes, parent tries) ── */
html, body, .stApp {
background-color: #060606 !important;
}
/* Aggressive full-viewport dark fill */
body::before {
content: '';
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: #060606;
z-index: -1;
pointer-events: none;
}
/* Darken any iframe wrapper or embed container */
iframe[title="streamlit_folium"],
iframe[src*="streamlit"],
[data-testid="stAppViewContainer"],
.main,
.block-container {
background-color: #060606 !important;
}
/* ── Global responsive breakpoints ── */
/* Tablets / small laptops (≤1024px) */
@media (max-width: 1024px) {
/* Shrink form column gaps */
div[data-testid="column"] {
padding-left: 4px !important;
padding-right: 4px !important;
}
/* Reduce selectbox font sizes */
.stSelectbox label, .stSelectbox div[data-baseweb="select"] {
font-size: 13px !important;
}
/* Shrink form button text */
.stForm button p {
font-size: 13px !important;
}
}
/* Narrow tablets / large phones (≤768px) */
@media (max-width: 768px) {
/* Hero section compact */
.hero-section {
min-height: 400px !important;
padding: 40px 16px 20px !important;
}
.hero-icon {
font-size: 72px !important;
}
.hero-title {
font-size: 28px !important;
}
.hero-subtitle {
font-size: 16px !important;
}
.hero-card {
width: 100% !important;
max-width: 280px !important;
padding: 16px 20px !important;
}
/* Results container */
div[data-testid="stVerticalBlock"] > div.stScrollableContainer {
height: auto !important;
max-height: 500px !important;
}
/* Map container */
iframe[title="streamlit_folium"] {
height: 400px !important;
}
/* Card description font size */
.card-desc {
font-size: 13px !important;
}
}
/* Phones (≤480px) */
@media (max-width: 480px) {
/* Card expander font */
summary {
font-size: 13px !important;
}
}
</style>
"""
CARD_EQUALIZER_JS = """
<script>
(function() {
// st.components renders in an iframe — reach out to the parent document
const doc = window.parent.document;
function equalizeCardDescriptions() {
const expanders = doc.querySelectorAll('.stExpander details[open]');
if (!expanders.length) {
// Cards not yet rendered — retry
setTimeout(equalizeCardDescriptions, 300);
return;
}
const rows = {};
expanders.forEach(details => {
const rect = details.getBoundingClientRect();
const rowKey = Math.round(rect.top / 20) * 20;
if (!rows[rowKey]) rows[rowKey] = [];
rows[rowKey].push(details);
});
Object.values(rows).forEach(rowItems => {
// Reset all description heights in the row
rowItems.forEach(details => {
const pTags = details.querySelectorAll('.stMarkdown p');
for (const p of pTags) {
if (!p.textContent.startsWith('💡') && !p.closest('.stMarkdown').querySelector('img')) {
p.closest('.stMarkdown').style.minHeight = '';
}
}
});
// Measure tallest description
let maxH = 0;
const descs = [];
rowItems.forEach(details => {
const pTags = details.querySelectorAll('.stMarkdown p');
for (const p of pTags) {
const parent = p.closest('.stMarkdown');
if (parent && !p.textContent.startsWith('💡') && !parent.querySelector('img')) {
const h = parent.getBoundingClientRect().height;
if (h > maxH) maxH = h;
descs.push(parent);
break;
}
}
});
// Set all to tallest
descs.forEach(desc => { desc.style.minHeight = maxH + 'px'; });
});
}
// Start with a delay to let Streamlit render cards
setTimeout(equalizeCardDescriptions, 500);
// Watch for DOM changes in the parent
new MutationObserver(() => {
clearTimeout(window._cardEqTimer);
window._cardEqTimer = setTimeout(equalizeCardDescriptions, 200);
}).observe(doc.body, { childList: true, subtree: true });
})();
</script>
"""
EMOJI_MAP = {
"attractions": "✨",
}
FLEX_PANELS_JS = """<!DOCTYPE html>
<html>
<body>
<script>
(function() {
// We run inside a Streamlit component iframe — target the parent document
const doc = window.parent.document;
let _resizing = false;
function resizePanels() {
if (_resizing) return;
_resizing = true;
const vh = window.parent.innerHeight;
if (!vh) { _resizing = false; return; }
// Strategy: find the scrollable card container and map iframe,
// then set their height so they fill the remaining viewport.
// Use getBoundingClientRect for accurate positioning.
const cardContainer = Array.from(
doc.querySelectorAll('[data-testid="stVerticalBlock"]')
).find(el => doc.defaultView.getComputedStyle(el).overflowY === 'auto');
if (cardContainer) {
// In narrow/stacked mode, let cards flow naturally without height cap
if (doc.body.classList.contains('stack-columns')) {
cardContainer.style.setProperty('height', 'auto', 'important');
cardContainer.style.setProperty('max-height', 'none', 'important');
if (cardContainer.parentElement?.getAttribute('data-testid') === 'stLayoutWrapper') {
cardContainer.parentElement.style.setProperty('height', 'auto', 'important');
}
} else {
const rect = cardContainer.getBoundingClientRect();
const panelHeight = Math.max(300, Math.min(vh - rect.top - 24, vh * 0.85));
cardContainer.style.setProperty('height', panelHeight + 'px', 'important');
cardContainer.style.setProperty('max-height', panelHeight + 'px', 'important');
// Also resize parent LayoutWrapper
if (cardContainer.parentElement?.getAttribute('data-testid') === 'stLayoutWrapper') {
cardContainer.parentElement.style.setProperty('height', panelHeight + 'px', 'important');
}
}
}
// Find the folium iframe container and set its height similarly
const foliumContainer = doc.querySelector('.stCustomComponentV1');
if (foliumContainer) {
foliumContainer.style.setProperty('max-height', '800px', 'important');
const rect = foliumContainer.getBoundingClientRect();
const mapHeight = Math.max(300, Math.min(vh - rect.top - 24, vh * 0.85));
foliumContainer.style.setProperty('height', mapHeight + 'px', 'important');
}
doc.querySelectorAll('.stCustomComponentV1 iframe').forEach(iframe => {
iframe.style.setProperty('max-height', '800px', 'important');
const rect = iframe.getBoundingClientRect();
const mapHeight = Math.max(300, Math.min(vh - rect.top - 24, vh * 0.85));
iframe.style.setProperty('height', mapHeight + 'px', 'important');
});
_resizing = false;
}
// Run on load (delayed to let Streamlit render)
setTimeout(resizePanels, 200);
// Run on resize
window.parent.addEventListener('resize', () => {
clearTimeout(window._panelResizeTimer);
window._panelResizeTimer = setTimeout(resizePanels, 100);
});
// Watch for DOM changes in parent (Streamlit re-renders)
new MutationObserver(() => {
clearTimeout(window._panelResizeTimer);
window._panelResizeTimer = setTimeout(resizePanels, 300);
}).observe(doc.body, { childList: true, subtree: true });
})();
</script>
</body>
</html>
"""
CARD_HOVER_JS = """<!DOCTYPE html>
<html>
<body>
<script>
(function() {
const doc = window.parent.document;
function getFoliumWin() {
var iframe = doc.querySelector('.stFolium iframe, iframe[title="streamlit_folium.st_folium"]');
return iframe ? iframe.contentWindow || iframe.contentWindow : null;
}
function getFoliumDoc() {
var iframe = doc.querySelector('.stFolium iframe, iframe[title="streamlit_folium.st_folium"]');
return iframe ? iframe.contentDocument || iframe.contentWindow.document : null;
}
function findLeafletMap() {
var win = getFoliumWin();
if (!win) return null;
// Leaflet map instances are stored as global variables; find one
for (var k in win) {
try {
if (win[k] && win[k]._container && win[k]._layers) return win[k];
} catch(e) {}
}
return null;
}
function highlightMarker(idx) {
var fdoc = getFoliumDoc();
if (!fdoc) return;
var el = fdoc.querySelector('.spider-marker[data-idx="'+idx+'"]');
if (!el) return;
el.style.background = '#f59e0b';
el.style.transform = 'scale(1.35)';
el.style.boxShadow = '0 0 14px rgba(245,158,11,0.6)';
el.style.zIndex = '1000';
// Open popup
var map = findLeafletMap();
if (map) {
map.eachLayer(function(layer) {
if (layer._icon === el.parentElement && layer._map) {
layer.openPopup();
}
});
}
}
function unhighlightMarker(idx) {
var fdoc = getFoliumDoc();
if (!fdoc) return;
var el = fdoc.querySelector('.spider-marker[data-idx="'+idx+'"]');
if (!el) return;
el.style.background = '#2a9fd6';
el.style.transform = '';
el.style.boxShadow = '0 2px 6px rgba(0,0,0,0.5)';
el.style.zIndex = '';
// Close popup
var map = findLeafletMap();
if (map) {
map.eachLayer(function(layer) {
if (layer._icon === el.parentElement && layer._map) {
layer.closePopup();
}
});
}
}
function setupCardHover() {
var pins = doc.querySelectorAll('.card-pin[data-card-idx]');
if (!pins.length) { setTimeout(setupCardHover, 300); return; }
pins.forEach(function(pin) {
if (pin._hoverSetup) return;
pin._hoverSetup = true;
var idx = parseInt(pin.getAttribute('data-card-idx'));
var column = pin.closest('[data-testid="stColumn"]') || pin.parentElement;
var expander = column ? column.querySelector('.stExpander') : null;
if (!expander) return;
expander.addEventListener('mouseenter', function() {
highlightMarker(idx);
});
expander.addEventListener('mouseleave', function() {
unhighlightMarker(idx);
});
});
}
setTimeout(setupCardHover, 500);
new MutationObserver(function() {
clearTimeout(window._hoverObTimer);
window._hoverObTimer = setTimeout(setupCardHover, 300);
}).observe(doc.body, { childList: true, subtree: true });
})();
</script>
</body>
</html>
"""
SMART_IMAGE_POSITION_JS = """<!DOCTYPE html>
<html>
<body>
<script>
(function() {
var doc = window.parent.document;
function repositionPortraitImages() {
var imgs = doc.querySelectorAll('.card-img');
var found = 0;
imgs.forEach(function(img) {
// If natural dimensions are available, check immediately
if (img.naturalHeight > 0 && img.naturalWidth > 0) {
found++;
if (img.naturalHeight > img.naturalWidth) {
// Portrait: show upper third to capture the attraction, not the ground
img.style.objectPosition = '50% 25%';
} else {
img.style.objectPosition = '50% 50%';
}
}
});
// Retry if no images have loaded yet
if (found === 0 && imgs.length > 0) {
setTimeout(repositionPortraitImages, 300);
}
}
// Also handle lazy-loaded images — they'll fire 'load' after becoming visible
doc.addEventListener('load', function(e) {
if (e.target && e.target.classList && e.target.classList.contains('card-img')) {
if (e.target.naturalHeight > e.target.naturalWidth) {
e.target.style.objectPosition = '50% 25%';
}
}
}, true);
// Initial run after DOM settles
setTimeout(repositionPortraitImages, 500);
})();
</script>
</body>
</html>
"""
FORCE_DARK_WRAPPER_JS = """<!DOCTYPE html>
<script>
(function() {
/* Best-effort attempt to force dark on parent wrapper (HF Spaces).
Cross-origin restrictions silently fail — no breakage. */
try {
if (window.parent !== window) {
var p = window.parent;
var html = p.document.documentElement;
var body = p.document.body;
if (html) {
html.style.backgroundColor = '#060606';
html.classList.add('dark');
}
if (body) body.style.backgroundColor = '#060606';
/* Also try to find and style the main wrapper container */
var containers = p.document.querySelectorAll('.SpacePage, .main-container, main, [class*="container"]');
for (var i = 0; i < containers.length; i++) {
var bg = getComputedStyle(containers[i]).backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
containers[i].style.backgroundColor = '#060606';
}
}
}
} catch(e) {
/* Cross-origin iframe — can't access parent. This is expected. */
}
})();
</script>"""
STACK_CONTROLS_JS = """<!DOCTYPE html>
<html>
<body>
<script>
(function() {
try {
var p = window.parent;
if (!p) return;
// Fixed breakpoint matching CSS @media (max-width: 768px)
var BREAKPOINT = 768;
function updateStack() {
var body = p.document.body;
if (!body) return;
var isNarrow = p.window.innerWidth <= BREAKPOINT;
body.classList.toggle('stack-columns', isNarrow);
// Apply 4-row layout to search controls (first horizontal block) only
var firstHB = body.querySelector('[data-testid="stHorizontalBlock"]');
if (isNarrow && firstHB) {
var cols = firstHB.querySelectorAll('[data-testid="stColumn"]');
// Columns 3-6: 50% width (Picks, Translation, Search, Refresh)
for (var i = 2; i < 6 && i < cols.length; i++) {
cols[i].style.flex = '0 1 calc(50% - 7.5px)';
}
// Spacing above each control row
for (var i = 0; i < cols.length; i++) {
var inner = cols[i].querySelector('div');
if (inner) inner.style.paddingTop = '12px';
}
} else if (firstHB) {
// Clear inline styles when unstacked
var cols = firstHB.querySelectorAll('[data-testid="stColumn"]');
for (var i = 0; i < cols.length; i++) {
cols[i].style.flex = '';
var inner = cols[i].querySelector('div');
if (inner) inner.style.paddingTop = '';
}
}
if (isNarrow) {
// Hide the map container directly (more reliable than CSS alone)
var mapContainer = body.querySelector('.stCustomComponentV1');
if (mapContainer) {
mapContainer.style.setProperty('display', 'none', 'important');
}
// Hide the map title
var mapTitles = body.querySelectorAll('h3');
for (var i = 0; i < mapTitles.length; i++) {
if (mapTitles[i].textContent.trim() === '🗺️ Map') {
mapTitles[i].style.setProperty('display', 'none', 'important');
}
}
} else {
// Restore map visibility
var mapContainer = body.querySelector('.stCustomComponentV1');
if (mapContainer) {
mapContainer.style.display = '';
}
var mapTitles = body.querySelectorAll('h3');
for (var i = 0; i < mapTitles.length; i++) {
if (mapTitles[i].textContent.trim() === '🗺️ Map') {
mapTitles[i].style.display = '';
}
}
}
}
// Collapse all cards except the first when stacking activates
function collapseCards() {
if (p.window.innerWidth > BREAKPOINT) return;
var body = p.document.body;
if (!body) return;
var expanders = body.querySelectorAll('.stExpander details');
for (var i = 0; i < expanders.length; i++) {
if (i === 0) {
expanders[i].setAttribute('open', '');
} else {
expanders[i].removeAttribute('open');
}
}
}
// Run on load — collapse cards after Streamlit renders them
if (document.readyState === 'complete') {
setTimeout(updateStack, 100);
setTimeout(collapseCards, 300);
setTimeout(collapseCards, 1000);
} else {
p.window.addEventListener('load', function() {
setTimeout(updateStack, 100);
setTimeout(collapseCards, 300);
setTimeout(collapseCards, 1000);
});
}
// Debounced resize — update layout and re-collapse
var timer;
p.window.addEventListener('resize', function() {
clearTimeout(timer);
timer = setTimeout(function() {
updateStack();
collapseCards();
}, 100);
});
// Watch for DOM changes — Streamlit renders content via websocket
// Only update layout (map hide/show, search controls), NEVER collapse
// so user can freely tap to expand/collapse cards
var moTimer;
var observer = new MutationObserver(function() {
clearTimeout(moTimer);
moTimer = setTimeout(updateStack, 200);
});
if (p.document.body) {
observer.observe(p.document.body, { childList: true, subtree: true });
}
} catch(e) {
/* Cross-origin — silently skip */
}
})();
</script>
</body>
</html>"""
def apply_dark_theme():
"""Inject dark-theme CSS, flexible panel JS, card↔map hover JS, and smart image positioning JS."""
import streamlit as st
st.markdown(DARK_THEME_CSS, unsafe_allow_html=True)
# Use st.iframe to execute JS (st.markdown strips <script> tags)
st.iframe(FORCE_DARK_WRAPPER_JS, height=1)
st.iframe(FLEX_PANELS_JS, height=1)
st.iframe(CARD_HOVER_JS, height=1)
st.iframe(SMART_IMAGE_POSITION_JS, height=1)
st.iframe(STACK_CONTROLS_JS, height=1)