diff --git a/src/gui/_home.py b/src/gui/_home.py
index a30af7a..b59aa8e 100644
--- a/src/gui/_home.py
+++ b/src/gui/_home.py
@@ -279,7 +279,16 @@ def _home_page() -> None:
digest = hashlib.sha1(
name.encode("utf-8"), usedforsecurity=False,
).hexdigest()[:10]
- col_name, col_size, col_x = st.columns([8, 1.6, 0.55])
+ # X button on the LEFT of the row per UX feedback —
+ # ``✕ | filename + chip | size``.
+ col_x, col_name, col_size = st.columns([0.55, 8, 1.6])
+ if col_x.button(
+ "✕",
+ key=f"_home_remove_{digest}",
+ help=f"Remove {name}",
+ type="tertiary",
+ ):
+ to_remove = name
col_name.markdown(
'
'
f'{_DOC_SVG}'
@@ -294,13 +303,6 @@ def _home_page() -> None:
'
',
unsafe_allow_html=True,
)
- if col_x.button(
- "✕",
- key=f"_home_remove_{digest}",
- help=f"Remove {name}",
- type="tertiary",
- ):
- to_remove = name
# In-card "Add more files" — clicks the (off-screen)
# ``stFileUploaderDropzoneInput`` so the OS file picker opens.
# Inline ``onclick`` would be cleanest but Streamlit's HTML
diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py
index 2661bc2..67b1fb5 100644
--- a/src/gui/components/_legacy.py
+++ b/src/gui/components/_legacy.py
@@ -179,6 +179,50 @@ body, .stApp {
background: #f5f4ef !important;
}
+/* Brand block at the top of the sidebar (mockup §brand) — a 28px
+ ink-filled rounded square with the wordmark "D" + "DataTools"
+ text. Injected into ``[data-testid="stSidebarHeader"]`` by the JS
+ below; ``stLogoSpacer`` is hidden so the brand block takes its
+ place flush against the left edge of the sidebar header. */
+.dt-brand {
+ display: flex !important;
+ align-items: center;
+ gap: 10px;
+ padding: 0 0 0 4px;
+ margin: 0;
+ height: 100%;
+ flex: 1;
+}
+.dt-brand-mark {
+ width: 28px;
+ height: 28px;
+ border-radius: 7px;
+ background: var(--ink);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--accent-fill);
+ font-family: var(--font-sans);
+ font-weight: 600;
+ font-size: 16px;
+ letter-spacing: -0.02em;
+ line-height: 1;
+ flex-shrink: 0;
+}
+.dt-brand-name {
+ font-family: var(--font-sans);
+ font-weight: 600;
+ font-size: 16px;
+ letter-spacing: -0.02em;
+ color: var(--ink);
+ line-height: 1;
+}
+/* The stock Streamlit logo placeholder takes 100x32 of space; hide
+ it so the injected brand has room to breathe. */
+[data-testid="stLogoSpacer"]:not(:has(.dt-brand)) {
+ display: none !important;
+}
+
/* Section labels in the page-nav: tiny uppercase tracking — the
"Eyebrow" row from spec §4. Streamlit renders these as nodes
with class ``st-emotion-cache-…`` inside ``stSidebarNav`` — class
@@ -200,16 +244,25 @@ body, .stApp {
margin: 0 !important;
}
-/* Nav items: comfortable padding, soft hover. */
+/* Nav items — tight padding so the menu lists feel dense and don't
+ waste vertical space. */
[data-testid="stSidebarNav"] a[data-testid="stSidebarNavLink"],
[data-testid="stSidebarNav"] [data-testid="stSidebarNavLinkContainer"] a {
color: var(--ink-secondary) !important;
font-size: 13.5px !important;
- line-height: 1.3 !important;
- padding: 7px 10px !important;
+ line-height: 1.25 !important;
+ padding: 4px 10px !important;
border-radius: var(--r-sm) !important;
transition: background 0.12s ease, color 0.12s ease;
}
+[data-testid="stSidebarNav"] li,
+[data-testid="stSidebarNavItems"] > li {
+ margin-bottom: 1px !important;
+}
+[data-testid="stSidebarNavSectionHeader"] {
+ padding-top: 10px !important;
+ padding-bottom: 2px !important;
+}
[data-testid="stSidebarNav"] a[data-testid="stSidebarNavLink"]:hover,
[data-testid="stSidebarNav"] [data-testid="stSidebarNavLinkContainer"] a:hover {
background: rgba(0,0,0,0.04) !important;
@@ -393,6 +446,12 @@ div[data-testid="stContainer"][data-border="true"] {
border-radius: var(--r-lg) !important;
box-shadow: 0 1px 2px rgba(28,25,23,0.03);
}
+/* Tighten the inter-row gap inside bordered containers — applies to
+ the Files card rows after import and the findings-card rows alike,
+ so the dense card body has less wasted vertical whitespace. */
+[data-testid="stVerticalBlockBorderWrapper"] [data-testid="stVerticalBlock"] {
+ gap: 0.25rem !important;
+}
/* ---------- Alerts — soft fills, no harsh borders ---------- */
[data-testid="stAlert"] [data-testid="stAlertContainer"],
@@ -438,6 +497,39 @@ div[data-testid="stContainer"][data-border="true"] {
font-family: var(--font-sans) !important;
}
+/* Sidebar widget labels — render as the "Eyebrow" row from spec §4
+ (tiny uppercase tracking, tertiary ink) so the ``Language`` /
+ ``Core · 1820 days left`` blocks at the bottom of the sidebar
+ match the section-title rhythm of the nav above. */
+[data-testid="stSidebar"] [data-testid="stWidgetLabel"] p,
+[data-testid="stSidebar"] label[data-testid="stWidgetLabel"] {
+ font-size: 11.5px !important;
+ font-weight: 500 !important;
+ text-transform: uppercase !important;
+ letter-spacing: 0.08em !important;
+ color: var(--ink-tertiary) !important;
+ margin-bottom: 4px !important;
+}
+/* Sidebar selectbox — quiet outline, cream surface that reads as
+ part of the sidebar rather than a Streamlit-default white island. */
+[data-testid="stSidebar"] [data-testid="stSelectbox"] div[role="combobox"],
+[data-testid="stSidebar"] [data-baseweb="select"] > div {
+ background: var(--surface) !important;
+ border: 1px solid var(--border) !important;
+ border-radius: var(--r-sm) !important;
+ font-size: 13px !important;
+ min-height: 32px !important;
+}
+[data-testid="stSidebar"] [data-testid="stSelectbox"] div[role="combobox"]:hover,
+[data-testid="stSidebar"] [data-baseweb="select"] > div:hover {
+ border-color: var(--border-strong) !important;
+}
+/* Streamlit pads the selectbox internals; tighten the chevron column
+ so the control isn't taller than the nav items above it. */
+[data-testid="stSidebar"] [data-baseweb="select"] > div > div {
+ padding: 4px 8px !important;
+}
+
/* Divider — softer warm gray instead of cool Streamlit default. */
[data-testid="stMarkdownContainer"] hr,
.stApp hr {
@@ -605,15 +697,57 @@ div[data-testid="stContainer"][data-border="true"] {
display: flex;
align-items: center;
gap: 12px;
- padding: 14px 18px;
+ /* Generous left/right padding so the filename + counts have visible
+ breathing room against the card's rounded edges — the head bleeds
+ out to those edges via the negative margin below, so without the
+ extra padding the content sits flush against the border. */
+ padding: 16px 22px;
border-bottom: 1px solid var(--border);
background: var(--surface-hover);
/* -1rem on top/sides bleeds the head to the card edges (the parent
- ``st.container(border=True)`` has 1rem padding). +0.75rem on the
- bottom is the breathing room before the first finding row —
- without it the row sits flush against the head's bottom border. */
- margin: -1rem -1rem 0.75rem;
+ ``st.container(border=True)`` has 1rem padding). +1.5rem on the
+ bottom is breathing room before the first finding row — without
+ it the row sits flush against the head's bottom border. */
+ margin: -1rem -1rem 1.5rem;
border-radius: var(--r-lg) var(--r-lg) 0 0;
+ cursor: pointer;
+ user-select: none;
+ transition: background 0.12s ease;
+}
+.dt-finding-group-head:hover {
+ background: var(--accent-fill);
+}
+/* Chevron lives on the right of the head, rotates to indicate state. */
+.dt-finding-group-chevron {
+ margin-left: 8px;
+ color: var(--ink-tertiary);
+ font-family: "Material Symbols Outlined" !important;
+ font-size: 20px !important;
+ font-feature-settings: normal !important;
+ line-height: 1 !important;
+ transition: transform 0.15s ease;
+ flex-shrink: 0;
+}
+.dt-finding-group-head[data-dt-collapsed="false"] .dt-finding-group-chevron {
+ transform: rotate(90deg);
+}
+/* Collapsed = body rows hidden + head tucks tight against card bottom.
+ The head's siblings inside the bordered container are the
+ ``stHorizontalBlock``s emitted by each ``st.columns`` row — when the
+ head carries ``data-dt-collapsed="true"`` they collapse to nothing
+ and the head's bottom border becomes the card's bottom edge. */
+.dt-finding-group-head[data-dt-collapsed="true"] {
+ margin: -1rem -1rem -1rem;
+ border-bottom: none;
+ border-radius: var(--r-lg);
+}
+/* Hide every sibling that comes AFTER the head's element-container
+ (the rows are emitted as ``stLayoutWrapper`` or
+ ``stElementContainer`` siblings depending on Streamlit's internal
+ layout reducer; ``~ *`` matches both and survives future renames). */
+[data-testid="stElementContainer"]:has(.dt-finding-group-head[data-dt-collapsed="true"])
+ ~ * {
+ display: none !important;
}
.dt-severity-dot {
width: 8px; height: 8px;
@@ -770,6 +904,85 @@ div[data-testid="stContainer"][data-border="true"] {
# script walks the dropzone buttons after first paint and rewrites the
# label to "Import" — and re-runs on Streamlit's component-rerender
# DOM mutations so the swap survives navigation and reruns.
+# Injects the sidebar brand block (mockup §brand) at the top of
+# Streamlit's ``stSidebarHeader``: the 28px ink-filled rounded square
+# with the "D" wordmark followed by the "DataTools" word. Streamlit's
+# ``stLogoSpacer`` reserves the slot but doesn't render anything
+# without a ``st.logo()`` call; we replace its content rather than
+# call ``st.logo`` because the brand wants both a chip AND wordmark
+# in one block, which ``st.logo`` can't do without shipping a static
+# image asset. MutationObserver re-injects when Streamlit remounts
+# the sidebar header.
+_INJECT_BRAND_JS = """
+
+"""
+
+
+# Toggle a ``.dt-finding-group-head``'s ``data-dt-collapsed`` attribute
+# on click. CSS handles the visual collapse (hide siblings, tuck the
+# head against the card bottom) — all this script does is flip the
+# attribute. MutationObserver re-binds when Streamlit remounts heads.
+_WIRE_COLLAPSIBLE_FINDINGS_JS = """
+
+"""
+
+
_RENAME_UPLOAD_BUTTON_JS = """