diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d4be047..04b529d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ # Changelog +## Sprint 180 Premium workbench UX hardening (2026-07-14) + +- Rebuilt the workbench presentation hierarchy around grouped task navigation, a compact Mol context header and an optional selection-detail drawer instead of a permanent third column. +- Fixed the narrow-screen shell so navigation is horizontal and the active workspace receives the full viewport width; desktop and ultrawide content now use stable, centered work areas. +- Reworked Overview into one operational status band, one workflow rail and compact quick actions without changing readiness calculations or routing. +- Made Data operational with three bounded project/AOI/dataset columns, scroll-safe populated catalogs and progressively disclosed create/upload forms. +- Made Map controls and MapLibre the primary surface while collapsing provenance, BBox inputs and raw feature inspection into explicit detail disclosures. +- Made AI Labs task-first by placing detection/segmentation run controls before model-registry diagnostics and collapsing registry/preflight detail. +- Added focused UI regression coverage; no API contract, migration, persistence, GIS operation, QA metric, model dependency or inference behavior changed. + ## Sprint 179 Mol detection evidence diagnosis (2026-07-13) - Added a read-only, storage-confined false-negative contact-sheet renderer that projects persisted missed GRB geometries onto the exact source tile manifest recorded by the analysis run. diff --git a/backend/tests/test_sprint180_premium_workbench.py b/backend/tests/test_sprint180_premium_workbench.py new file mode 100644 index 00000000..27c2a0e9 --- /dev/null +++ b/backend/tests/test_sprint180_premium_workbench.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def test_workbench_uses_grouped_navigation_and_optional_inspector() -> None: + app = read("frontend/src/App.tsx") + inspector = read("frontend/src/components/inspector/WorkbenchInspector.tsx") + + assert "import './styles/premium.css'" in app + assert "const workspaceNavGroups" in app + assert "['overview', 'data', 'map']" in app + assert "['analysis', 'ai']" in app + assert "const [inspectorOpen, setInspectorOpen] = useState(false)" in app + assert "{inspectorOpen ? (" in app + assert 'id="workbench-inspector"' in app + assert "onClose={() => setInspectorOpen(false)}" in app + assert 'className="inspector-close"' in inspector + + +def test_data_creation_forms_are_progressively_disclosed() -> None: + project = read("frontend/src/components/project/ProjectPanel.tsx") + area = read("frontend/src/components/project/AreaPanel.tsx") + dataset = read("frontend/src/components/datasets/DatasetPanel.tsx") + css = read("frontend/src/styles/premium.css") + + assert '
' in project + assert 'Create project' in project + assert '
' in area + assert 'Create spatial scope' in area + assert '
' in dataset + assert 'Upload source data' in dataset + assert "details.data-panel-form-block > summary" in css + assert ".workspace-grid-data > section:nth-child(n)" in css + assert "max-height: calc(100dvh - 10.5rem);" in css + + +def test_map_prioritizes_controls_map_and_collapsed_diagnostics() -> None: + map_workspace = read("frontend/src/components/map/MapWorkspace.tsx") + css = read("frontend/src/styles/premium.css") + + control_index = map_workspace.index('className="map-control-surface"') + map_index = map_workspace.index('className="map-frame-surface"') + detail_index = map_workspace.index('className="map-layer-details"') + assert control_index < map_index < detail_index + assert '
' in map_workspace + assert '
' in map_workspace + assert 'className="map-advanced-tools-body"' in map_workspace + assert ".map-control-surface {\n order: 1;" in css + assert ".map-frame-surface {\n order: 2;" in css + assert ".map-layer-details {\n order: 3;" in css + + +def test_ai_workspaces_prioritize_runs_and_collapse_registry_detail() -> None: + detection = read("frontend/src/components/detection/DetectionLab.tsx") + segmentation = read("frontend/src/components/segmentation/SegmentationLab.tsx") + css = read("frontend/src/styles/premium.css") + + assert '
' in detection + assert '
' in detection + assert '
' in segmentation + assert ".ai-lab-shell > .lab-block {\n order: 2;" in css + assert ".ai-lab-shell > .ai-lab-model-surface {\n order: 7;" in css + assert "details.ai-lab-model-surface > summary" in css + + +def test_mobile_shell_uses_full_width_main_and_horizontal_navigation() -> None: + css = read("frontend/src/styles/premium.css") + + assert "@media (max-width: 920px)" in css + assert ".app-shell > header.workbench-topbar" in css + assert ".workbench-layout {\n display: block;" in css + assert ".workbench-sidebar nav {\n display: flex;" in css + assert ".nav-group {\n display: contents;" in css + assert ".workbench-inspector {\n top: 0;\n width: 100vw;" in css + assert "grid-auto-flow: column;" in css + assert "overflow-x: auto;" in css diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 01226383..c80236c5 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -7345,3 +7345,42 @@ Open: - Browser verification on `http://192.168.10.150:1202` selected the complete Mol Postel workspace and showed ready project/AOI/dataset/QA/export state. - The Map workspace rendered one `1033x542` MapLibre canvas, the OSM road basemap, AOI and `95` persisted detections with no browser warnings/errors, no visible dialog and no horizontal document overflow at a `1280 px` viewport. - No API contract, migration, QualityCheck/Metric row, Detection row, model asset, active-model configuration or inference result changed in this pass. + +# Sprint 180 - Premium workbench UX hardening + +## Implementation + +- Reworked the workbench shell into a calmer operational hierarchy with grouped Workspace, Analyze and Deliver navigation, a compact Mol/Kempen context header and a centered content canvas. +- Replaced the permanently reserved inspector column with an on-demand detail drawer. Dataset inspection still invokes the existing detail-loading callback and now opens the drawer explicitly. +- Made Data operational at scale by disclosing create/upload forms on demand, arranging Project, AOI and Dataset panels side by side on desktop and bounding their long collections with internal scrolling. +- Promoted MapLibre to the primary Map surface. Layer provenance and low-frequency BBox/raw-feature controls remain available in collapsed detail surfaces without removing any GIS action. +- Reordered Detection and Segmentation Labs around run controls and result review; model registry and YOLO preflight remain fully available as secondary disclosures. +- Added a dedicated `premium.css` presentation layer with a restrained neutral/teal palette, consistent controls, stable panel dimensions and an actual full-width mobile shell. The former narrow sidebar-plus-content split is removed below `920 px`. +- Added focused static regression coverage for navigation grouping, optional inspector wiring, scalable Data panels, map hierarchy, AI Lab ordering and responsive behavior. + +## Behavior preservation + +- Existing React state hooks, API clients, service calls, map callbacks, QA actions, uploads, exports and AI run handlers were retained. +- No backend application code, endpoint contract, database model, migration, geospatial algorithm, model configuration or persisted evidence was changed. +- Legacy UI contract strings and callback signatures remain present for the existing Sprint 1-179 regression suite. + +## Visual audit + +- At `1280x720`, the Overview now uses one compact readiness row and no permanent inspector reservation; Map controls, canvas and secondary layer details follow the intended visual order. +- At `2560x1080`, the main workbench uses a centered `1680 px` maximum content width while the map can expand independently; the previous permanently empty `416 px` inspector column is gone. +- At `390x844`, navigation becomes a horizontal full-width rail and the main workspace occupies the viewport instead of sharing it with a `152 px` sidebar. +- A live 50-project Data state now keeps project and dataset panels within the viewport with internal scrolling instead of pushing the Dataset panel thousands of pixels below the fold. +- Detection and Segmentation run controls appear before registry diagnostics, reducing the distance to the primary task while retaining honest model-state detail. + +## Local validation + +- React best-practice review found no new conditional hooks, effect synchronization, unstable list keys or non-semantic interactive controls. +- Focused premium-workbench and legacy UI regression coverage passed. +- `python -m compileall backend/app`: passed. +- Full backend suite: `492 passed`. +- Frontend typecheck and production build: passed; app bundle `219.76 kB`, React vendor `140.74 kB` and MapLibre `801.82 kB` before gzip. +- `bash scripts/run_readiness_check.sh`: passed with the 81-route API contract audit, single Alembic head, all backend tests, frontend typecheck/build and live-smoke syntax gate. + +## Next pass + +- Harden detection QA coverage and matching diagnostics before making a retraining decision; keep that work separate from this presentation-only sprint. diff --git a/docs/TODO.md b/docs/TODO.md index 73f68150..3e69c259 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -402,6 +402,14 @@ This file now starts with the current implementation status. Older preparation/b # Current Workbench UI +- [x] Replace the accumulated shell overrides with a coherent premium presentation layer while preserving V1 behavior. +- [x] Group navigation into Workspace, Analyze and Deliver and make the inspector an optional drawer. +- [x] Fix mobile navigation so the active workspace uses the full viewport width. +- [x] Bound populated Project, Dataset, QA, AI and Export panels so long histories do not push core actions thousands of pixels down-page. +- [x] Move Data create/upload forms and Map/AI diagnostics into explicit progressive disclosures. +- [x] Reorder AI Labs around run controls and Map around layer selection plus the MapLibre canvas. +- [ ] Harden detection QA coverage and matching diagnostics before any further model training. + - [x] Replace the one-page workflow panel stack with a task-based workbench shell. - [x] Add persistent project/AOI/dataset/layer context. - [x] Move selected dataset details into a persistent inspector. diff --git a/frontend/README.md b/frontend/README.md index 78a83ed1..f7e5b3cd 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -4,13 +4,13 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow. Mol is the primary operating context. On a fresh session the workbench prefers a persisted Mol project or dataset, centers an empty MapLibre view on Mol and pre-fills a compact Mol AOI. Explicit project and dataset selections remain authoritative, and all broader Kempen workflows remain available. -The workbench now uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but the UI is organized into Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces with a persistent top context bar and right-side dataset inspector. +The workbench uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but the UI is organized into Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces with a persistent top context bar and an optional selection-detail drawer. -The shell has a calmer V1 workbench density pass: the duplicated workspace command bar is hidden, the sidebar is more compact, the top context bar is quieter, readiness tiles are smaller and panels use softer borders/shadows. This keeps the same workflows and API calls while reducing visual noise on desktop and mobile. +The premium V1 presentation layer lives in `src/styles/premium.css`. It groups navigation by Workspace, Analyze and Deliver, removes the permanent inspector column, gives desktop/ultrawide workspaces stable readable widths and switches narrow screens to full-width content with a horizontally scrollable navigation rail. API calls and workflow state remain owned by the existing hooks. -Data and Map have an additional usability layout pass for the core daily workflow. The Data workspace uses compact catalog cards, shorter action buttons and tighter role summaries. The Map workspace prioritizes the MapLibre frame before dense controls, gives the map more height on desktop and compresses context/provenance/bbox extraction surfaces while preserving the existing selection, export and QA actions. +Data and Map are organized around the core daily workflow. Data shows Project, AOI and Dataset columns together on normal desktop widths, bounds populated lists inside their own panels and keeps create/upload forms in explicit disclosures. Map keeps the layer/AOI command surface and MapLibre frame first, then exposes provenance, BBox controls and raw feature inspection only when requested. Existing selection, export and QA actions are unchanged. -Wide and ultrawide screens now get dedicated workbench breakpoints at `1800px` and `2200px`. These keep the sidebar and inspector readable, expand the MapLibre review frame and use extra horizontal space for Data, Analysis, AI and Export grids without changing API calls or workflows. +Wide and ultrawide screens keep a readable sidebar and centered work area, expand the MapLibre review frame and use extra horizontal space for Data, Analysis, AI and Export grids. The detail drawer overlays the work area only while open, so it does not permanently consume ultrawide canvas space. The Map workspace defaults to an OpenStreetMap road basemap with visible attribution so uploaded vectors, AOIs and QA overlays appear on a real street context. Set `VITE_MAP_STYLE_URL` to a managed MapLibre style URL to override this for production or high-volume deployments. @@ -288,7 +288,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst - The Overview workspace now also shows a compact workflow guidance rail for the V1 path from project and AOI setup through data, map review, QA/AI validation and export handoff. - When the V1 workflow is populated end to end, the Overview rail switches to a `Ready for handoff` state and keeps map guidance explicit about layer features versus AOI context. - Overview rail Map and Export clicks reuse the first ready vector/GeoJSON dataset when no layer or dataset is active, keeping the happy path connected without adding new API calls. -- The Data workspace surfaces selected project, AOI and dataset context before creation/upload forms, then separates form and catalog/list regions for faster scanning. +- The Data workspace surfaces selected project, AOI and dataset context first, keeps creation/upload forms in closed-by-default disclosures and constrains large populated catalogs to independently scrollable panels. - The Export Center includes a handoff readiness summary, grouped artifact actions and provenance-rich export cards so report/GeoJSON handoff stays understandable in long-running demo projects. - The Export Center highlights the latest project report, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON artifacts with direct preview/download actions. - The offline demo workflow now selects the seeded raster fixture for Detection and Segmentation Lab prerequisites while keeping the candidate vector dataset open for Data/Map/Export review. @@ -300,10 +300,11 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst - `workbench-topbar`: active project, AOI, dataset and layer context. - `workbench-sidebar`: primary navigation between workflow workspaces. - `workbench-main`: one active workflow at a time. - - `workbench-inspector`: persistent selected dataset details and raster/vector operation controls. + - `workbench-inspector`: optional selected dataset details and raster/vector operation controls, opened from the active workspace heading. - Existing API calls, hooks, MapLibre rendering and QA/AI/export flows are unchanged. - Stable navigation test anchors use `data-testid="workspace-nav-{workspace}"`. -- Mobile workbench navigation uses horizontal rails for the primary nav and command bar, avoiding a tall menu stack before the active workspace content. +- Mobile workbench navigation uses a full-width horizontal primary rail, avoiding the former fixed 152 px sidebar that compressed the active workspace. +- Detection and Segmentation Labs present run controls first; model registry and YOLO preflight diagnostics remain available as collapsed detail surfaces below the operational workflow. - The Map workspace shows active layer source/provenance/draw-state context and selected-feature property chips before the raw JSON inspector. - When the Map workspace has no active result layer, it lists ready vector/GeoJSON datasets as direct quick actions so populated demo projects can jump straight from the empty state to map inspection. - The Map workspace can extract persisted vector features by area: open a ready vector dataset, use `Start map bbox` and click two map corners or enter EPSG:4326 bbox values, then run `Run area extract` to query backend `vector_features`. Results are highlighted on the map and can be downloaded as GeoJSON. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e32b0aca..4c38a43c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import './styles/app.css' +import './styles/premium.css' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' import { DatasetPanel } from './components/datasets/DatasetPanel' import { DetectionLab } from './components/detection/DetectionLab' @@ -45,10 +46,18 @@ const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description: { key: 'system', label: 'System', description: 'Provider capabilities' }, ] +const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [ + { label: 'Workspace', keys: ['overview', 'data', 'map'] }, + { label: 'Analyze', keys: ['analysis', 'ai'] }, + { label: 'Deliver', keys: ['exports', 'system'] }, +] + function App(): JSX.Element { const [activeWorkspace, setActiveWorkspace] = useState('overview') + const [inspectorOpen, setInspectorOpen] = useState(false) useEffect(() => { window.scrollTo({ top: 0, left: 0 }) + setInspectorOpen(false) }, [activeWorkspace]) const { @@ -643,8 +652,11 @@ function App(): JSX.Element {
-

GeoAI Workbench · Mol / Kempen

-

GeoIntel Kempen

+ +
+

GeoIntel

+

GeoAI Workbench · Mol / Kempen

+
@@ -671,30 +683,54 @@ function App(): JSX.Element {
-
-

Workspace

+
+

{selectedProject?.region ?? 'Mol, Kempen'}

{activeWorkspaceItem.label}

-

{activeWorkspaceItem.description}

+
+

{activeWorkspaceItem.description}

+ {activeWorkspace !== 'overview' ? ( + + ) : null} +
@@ -763,12 +799,8 @@ function App(): JSX.Element {
-

Recommended flow

-

Build the project, load data, review on map, validate, export.

-

- Keep the daily workflow focused: prepare data first, inspect it spatially, then run QA/QC or AI lab tasks only when the - required datasets are present. -

+

Quick access

+

Continue working

- + /> + + ) : null}
) diff --git a/frontend/src/components/WorkbenchStatusStrip.tsx b/frontend/src/components/WorkbenchStatusStrip.tsx index c70ee87a..63c625e9 100644 --- a/frontend/src/components/WorkbenchStatusStrip.tsx +++ b/frontend/src/components/WorkbenchStatusStrip.tsx @@ -105,15 +105,23 @@ export function WorkbenchStatusStrip({ state: exports.length > 0 ? 'ready' : 'waiting', }, ] + const readyCount = items.filter((item) => item.state === 'ready').length return (

V1 readiness

-

Workbench status

+

+ Workbench status: + {readyCount === items.length ? 'Mol workbench is ready' : 'Mol workbench needs attention'} +

+

{nextAction(items)}

+
+
+ {readyCount}/{items.length} + stages ready
-

{nextAction(items)}

{items.map((item) => ( diff --git a/frontend/src/components/datasets/DatasetPanel.tsx b/frontend/src/components/datasets/DatasetPanel.tsx index e1b36882..1a0d621e 100644 --- a/frontend/src/components/datasets/DatasetPanel.tsx +++ b/frontend/src/components/datasets/DatasetPanel.tsx @@ -23,6 +23,7 @@ interface DatasetPanelProps { onDatasetFormChange: Dispatch> onUploadDataset: (event: FormEvent) => void onLoadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => void + onOpenInspector: () => void onRefreshMetadata: (datasetId: string) => void onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void onOpenDatasetExport: (dataset: DatasetCreateResponse) => void @@ -100,6 +101,7 @@ export function DatasetPanel({ onDatasetFormChange, onUploadDataset, onLoadDatasetDetails, + onOpenInspector, onRefreshMetadata, onOpenDatasetInMap, onOpenDatasetExport, @@ -153,8 +155,8 @@ export function DatasetPanel({ {selectedDataset ? `${datasetRoleLabel(normalizeDatasetRole(selectedDataset))} / ${selectedDataset.status}` : 'Inspect a dataset to load details and tools.'}
-
-

Upload source data

+
+ Upload source data
+
{roleSummaries.map((summary) => ( @@ -255,6 +257,7 @@ export function DatasetPanel({
-
-
-
-

Model registry

-

Backend-reported detector states and limitations.

-
-
+
+ + Model registry + {detectionModels.length} models + +
{loadingDetectionModels ? (
@@ -254,7 +253,8 @@ export function DetectionLab({ ))} -
+
+
{selectedDetectionModelId === 'yolo-configured' ? (
@@ -359,7 +359,12 @@ export function DetectionLab({
) : null} -
+
+ + YOLO runtime preflight + {yoloPreflight?.status ?? 'not loaded'} + +

YOLO runtime preflight

@@ -437,7 +442,8 @@ export function DetectionLab({
) : null} -
+
+
diff --git a/frontend/src/components/inspector/WorkbenchInspector.tsx b/frontend/src/components/inspector/WorkbenchInspector.tsx index bc0db326..d9280edf 100644 --- a/frontend/src/components/inspector/WorkbenchInspector.tsx +++ b/frontend/src/components/inspector/WorkbenchInspector.tsx @@ -35,6 +35,7 @@ interface WorkbenchInspectorProps { onOpenQualityWorkspace: () => void onOpenExportsWorkspace: () => void onOpenAiWorkspace: () => void + onClose: () => void } function formatValue(value: string | number | null | undefined): string { @@ -74,6 +75,7 @@ export function WorkbenchInspector({ onOpenQualityWorkspace, onOpenExportsWorkspace, onOpenAiWorkspace, + onClose, }: WorkbenchInspectorProps): JSX.Element { const [activeTab, setActiveTab] = useState('context') const selectedDetectionRun = useMemo( @@ -103,6 +105,9 @@ export function WorkbenchInspector({

Inspector

Selection details

+
{tabs.map((tab) => ( diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index ed08f3ab..d197adf8 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -489,29 +489,6 @@ export function MapWorkspace({ {mapFeatureCollection ? `${mapFeatureCount} features` : 'no layer'}
-
-
- Area of interest - {selectedMapArea?.name ?? 'No area selected'} - {areaFeatureCollection ? `${areaFeatureCount} AOI features loaded` : 'AOI overlay disabled'} -
-
- Active layer - {mapLayerLabel} - {mapLayerSourceLabel} -
-
- Feature state - {mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No layer rendered'} - {mapLayerProvenance} -
-
- QA/QC evidence - {qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} evidence features` : 'No evidence overlay'} - {qualityEvidenceLoading ? 'Loading persisted evidence' : 'Matches, false positives and false negatives'} -
-
-
{usesDefaultOsmBasemap ? (
@@ -605,6 +582,53 @@ export function MapWorkspace({ {mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}
+ + +
+ +
+ +
+ + Layer details + {mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No active layer'} + +
+
+ Area of interest + {selectedMapArea?.name ?? 'No area selected'} + {areaFeatureCollection ? `${areaFeatureCount} AOI features loaded` : 'AOI overlay disabled'} +
+
+ Active layer + {mapLayerLabel} + {mapLayerSourceLabel} +
+
+ Feature state + {mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No layer rendered'} + {mapLayerProvenance} +
+
+ QA/QC evidence + {qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} evidence features` : 'No evidence overlay'} + {qualityEvidenceLoading ? 'Loading persisted evidence' : 'Matches, false positives and false negatives'} +
+
Layer source @@ -623,7 +647,7 @@ export function MapWorkspace({ {qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} rendered` : 'off'}
- +
{qualityEvidenceGeoJson || qualityEvidenceError || qualityEvidenceWarnings.length > 0 ? (
@@ -671,24 +695,6 @@ export function MapWorkspace({
) : null} -
- -
-
@@ -863,6 +869,12 @@ export function MapWorkspace({ {fullWorkflowError ?

{fullWorkflowError}

: null}
+
+ + Advanced selection and inspection + BBox, feature extract and raw properties + +
@@ -1208,6 +1220,8 @@ export function MapWorkspace({

Click a visible map feature to inspect its properties.

)}
+
+
) diff --git a/frontend/src/components/project/AreaPanel.tsx b/frontend/src/components/project/AreaPanel.tsx index b8a28038..fd6db9e6 100644 --- a/frontend/src/components/project/AreaPanel.tsx +++ b/frontend/src/components/project/AreaPanel.tsx @@ -49,8 +49,8 @@ export function AreaPanel({ {selectedArea?.area_m2 ? `${selectedArea.area_m2.toFixed(2)} m2` : 'Select an AOI with geometry for spatial review.'} -
-

Create spatial scope

+
+ Create spatial scope
+
{loadingAreas ?

Loading areas...

: null} {areas.length === 0 ?

No areas yet

: null} diff --git a/frontend/src/components/project/ProjectPanel.tsx b/frontend/src/components/project/ProjectPanel.tsx index bfab8709..0b19e5e0 100644 --- a/frontend/src/components/project/ProjectPanel.tsx +++ b/frontend/src/components/project/ProjectPanel.tsx @@ -45,8 +45,8 @@ export function ProjectPanel({ {selectedProject?.region ?? 'Create or load a project to start the workbench.'} -
-

Create context

+
+ Create project
+
-
-
-
-

Model registry

-

Backend-reported segmenter states and limitations.

-
-
+
+ + Model registry + {segmentationModels.length} models + +
{loadingSegmentationModels ? (
@@ -158,7 +157,8 @@ export function SegmentationLab({ ))} -
+
+
diff --git a/frontend/src/styles/premium.css b/frontend/src/styles/premium.css new file mode 100644 index 00000000..ac54ed17 --- /dev/null +++ b/frontend/src/styles/premium.css @@ -0,0 +1,1482 @@ +/* Sprint 180 premium workbench: one coherent presentation layer over the preserved V1 workflows. */ + +:root { + --bg: #f2f5f7; + --panel: #ffffff; + --panel-soft: #f7f9fa; + --surface-raised: #ffffff; + --surface-sunken: #f5f7f8; + --text: #17212b; + --muted: #62707d; + --line: #d8e0e5; + --line-strong: #b7c3cb; + --accent: #08776d; + --accent-strong: #065d56; + --accent-soft: #e5f3f0; + --focus-ring: #08776d; + --focus-ring-soft: rgba(8, 119, 109, 0.18); + --warning: #a45a08; + --danger: #a33434; + --info: #2869a8; + --shadow: 0 18px 44px rgba(29, 43, 54, 0.12); + --shadow-soft: 0 5px 16px rgba(29, 43, 54, 0.07); +} + +html, +body { + background: var(--bg); +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + clip-path: inset(50%); +} + +body { + min-width: 0; + color: var(--text); + background-image: none; +} + +.workbench-shell, +.workbench-shell * { + letter-spacing: 0; +} + +.workbench-shell { + min-height: 100dvh; + background: var(--bg); +} + +.workbench-shell section, +.workbench-shell button { + background-image: none; +} + +.workbench-shell button { + border-color: var(--line-strong); + background: #ffffff; + box-shadow: none; + transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease, box-shadow 120ms ease; +} + +.workbench-shell button:hover:not(:disabled) { + border-color: #8fa3ae; + background: #f8fafb; + box-shadow: 0 0 0 3px rgba(40, 105, 168, 0.08); +} + +.workbench-shell .primary-action { + border-color: var(--accent); + background: var(--accent); + color: #ffffff; +} + +.workbench-shell .primary-action:hover:not(:disabled) { + border-color: var(--accent-strong); + background: var(--accent-strong); + box-shadow: 0 0 0 3px var(--focus-ring-soft); +} + +.workbench-shell input, +.workbench-shell select, +.workbench-shell textarea { + border-color: var(--line-strong); + background: #ffffff; + box-shadow: none; +} + +.workbench-shell .eyebrow, +.workbench-shell .nav-section-label, +.workbench-shell .context-bar span, +.workbench-shell .status-tile-topline > span:first-child, +.workbench-shell .inspector-field > span { + color: #6b7782; + font-size: 0.69rem; + font-weight: 750; + text-transform: uppercase; +} + +/* Shell */ + +.app-shell > header.workbench-topbar { + position: sticky; + top: 0; + z-index: 30; + display: grid; + grid-template-columns: minmax(15rem, 18rem) minmax(0, 1fr); + min-height: 4.5rem; + gap: 1rem; + align-items: center; + border-bottom: 1px solid var(--line); + padding: 0.65rem 1.25rem; + background: rgba(255, 255, 255, 0.98); + box-shadow: 0 1px 0 rgba(23, 33, 43, 0.03); + backdrop-filter: blur(14px); +} + +.brand-block { + display: flex; + min-width: 0; + gap: 0.7rem; + align-items: center; +} + +.brand-mark { + display: grid; + width: 2.25rem; + height: 2.25rem; + flex: 0 0 2.25rem; + place-items: center; + border-radius: 7px; + background: #123238; + color: #ffffff; + font-size: 0.78rem; + font-weight: 850; +} + +.brand-block h1 { + margin: 0; + font-size: 1.05rem; + line-height: 1.05; +} + +.app-shell > header.workbench-topbar .brand-block p { + margin: 0.18rem 0 0; + color: var(--muted); + font-size: 0.72rem; + font-weight: 600; +} + +.context-bar { + display: grid; + grid-template-columns: minmax(10rem, 1.25fr) repeat(3, minmax(8rem, 1fr)); + min-width: 0; + gap: 0; + justify-content: stretch; + border: 1px solid var(--line); + border-radius: 7px; + background: #f8fafb; +} + +.context-bar > div { + min-width: 0; + min-height: 2.9rem; + max-width: none; + border: 0; + border-right: 1px solid var(--line); + border-radius: 0; + padding: 0.45rem 0.7rem; + background: transparent; +} + +.context-bar > div:last-child { + border-right: 0; +} + +.context-bar span { + display: block; + font-size: 0.61rem; +} + +.context-bar strong { + display: block; + overflow: hidden; + margin-top: 0.16rem; + color: #27343f; + font-size: 0.78rem; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workbench-layout { + display: grid; + grid-template-columns: 12.25rem minmax(0, 1fr); + min-width: 0; + height: calc(100dvh - 4.5rem); + min-height: 0; +} + +.workbench-sidebar { + min-width: 0; + overflow: auto; + border-right: 1px solid var(--line); + padding: 0.9rem 0.7rem; + background: #f8fafb; +} + +.workbench-sidebar nav { + display: grid; + gap: 1rem; +} + +.nav-group { + display: grid; + gap: 0.2rem; +} + +.nav-section-label { + margin: 0 0 0.25rem; + padding: 0 0.55rem; + font-size: 0.63rem; +} + +.nav-item { + position: relative; + display: block; + width: 100%; + min-height: 2.7rem; + border: 1px solid transparent; + border-radius: 6px; + padding: 0.52rem 0.65rem; + background: transparent; + color: #34424d; + text-align: left; +} + +.nav-item span { + font-size: 0.86rem; + font-weight: 700; +} + +.nav-item small { + display: none; +} + +.nav-item-active { + border-color: #c8ded9; + background: var(--accent-soft); + color: var(--accent-strong); + box-shadow: inset 3px 0 0 var(--accent); +} + +.nav-item-active small { + display: block; + margin-top: 0.18rem; + color: #50746e; + font-size: 0.69rem; + font-weight: 550; + line-height: 1.2; +} + +.workbench-sidebar .nav-item { + border-color: transparent; + background: transparent; +} + +.workbench-sidebar .nav-item-active { + border-color: #c8ded9; + background: var(--accent-soft); +} + +.workbench-main { + min-width: 0; + min-height: 0; + overflow: auto; + padding: 1.35rem clamp(1rem, 2vw, 2rem) 2rem; + scroll-margin-top: 5rem; +} + +.workbench-main > * { + width: min(100%, 105rem); + margin-inline: auto; +} + +.workspace-heading { + display: flex; + gap: 1rem; + align-items: center; + justify-content: space-between; + margin-bottom: 1.1rem; + border: 0; + border-bottom: 1px solid var(--line); + border-radius: 0; + padding: 0 0 0.9rem; + background: transparent; + box-shadow: none; +} + +.workspace-heading .eyebrow { + margin: 0 0 0.2rem; +} + +.workspace-heading h2 { + margin: 0; + font-size: 1.55rem; + line-height: 1.1; +} + +.workspace-heading-actions { + display: flex; + min-width: 0; + gap: 0.75rem; + align-items: center; + justify-content: flex-end; +} + +.workspace-heading-actions > p { + max-width: 25rem; + margin: 0; + color: var(--muted); + font-size: 0.84rem; + line-height: 1.35; + text-align: right; +} + +.inspector-toggle, +.inspector-close { + width: auto; + min-height: 2.15rem; + flex: 0 0 auto; + padding: 0.38rem 0.65rem; + font-size: 0.78rem; +} + +.inspector-toggle-active { + border-color: var(--accent); + color: var(--accent-strong); +} + +.workspace-command-bar { + display: none; +} + +.workbench-inspector { + position: fixed; + top: 4.5rem; + right: 0; + bottom: 0; + z-index: 45; + width: min(28rem, 100vw); + min-width: 0; + overflow: auto; + border: 0; + border-left: 1px solid var(--line); + padding: 1rem; + background: #ffffff; + box-shadow: -18px 0 44px rgba(28, 42, 53, 0.16); +} + +.inspector-header { + position: sticky; + top: -1rem; + z-index: 2; + display: flex; + gap: 1rem; + align-items: center; + justify-content: space-between; + margin: -1rem -1rem 0.8rem; + border-bottom: 1px solid var(--line); + padding: 1rem; + background: rgba(255, 255, 255, 0.98); +} + +.inspector-header h2 { + margin: 0; + font-size: 1.15rem; +} + +.workbench-inspector-panel .inspector-header { + display: flex; + grid-template-columns: none; +} + +.workbench-inspector-panel .inspector-header .inspector-close { + width: auto; + min-width: 4.25rem; + margin: 0; +} + +.inspector-tabs { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.28rem; + border-bottom: 1px solid var(--line); + padding-bottom: 0.65rem; +} + +.inspector-tab { + min-width: 0; + min-height: 2.15rem; + padding: 0.35rem 0.4rem; + font-size: 0.74rem; +} + +.inspector-tab-active { + border-color: #b6d6d0; + background: var(--accent-soft); + color: var(--accent-strong); +} + +.inspector-card { + border: 0; + border-bottom: 1px solid var(--line); + border-radius: 0; + padding: 0.8rem 0 1rem; + background: transparent; +} + +.inspector-card h3 { + margin-top: 0; +} + +/* Shared hierarchy */ + +.workspace-stack, +.workspace-grid { + gap: 1rem; +} + +.workbench-main section, +.workspace-panel, +.map-control-surface, +.map-frame-surface, +.map-inspection-surface, +.quality-summary-surface, +.quality-control-surface, +.quality-evidence-surface, +.quality-history-surface, +.change-detection-input-surface, +.change-detection-result-surface, +.lab-block, +.handoff-action-card, +.system-provider-card { + border-color: var(--line); + border-radius: 7px; + background: #ffffff; + box-shadow: none; +} + +.panel-title-row { + gap: 0.75rem; + align-items: center; +} + +.panel-title-row h2, +.panel-title-row h3, +.workspace-panel h2, +.lab-block h3 { + margin-bottom: 0; +} + +.count-pill, +.status-pill, +.status-badge, +.workflow-step-status { + border-radius: 999px; + background: #edf2f4; + color: #4e5d68; + font-weight: 700; +} + +.data-panel-form-block, +.data-panel-list-block, +.result-summary-card, +.quality-metric-card, +.model-card, +.inspector-card, +.feature-property-chip, +.layer-control-card { + box-shadow: none; +} + +.table-scroll { + border-color: var(--line); + border-radius: 6px; +} + +table thead th { + background: #f4f7f8; + color: #53616d; +} + +table tbody tr:hover { + background: #f7fafb; +} + +.error, +.warning-message, +.success-message, +.demo-message { + border-radius: 6px; +} + +/* Data workspace */ + +details.data-panel-form-block { + overflow: hidden; + border: 1px solid var(--line); + border-radius: 6px; + padding: 0; + background: #f8fafb; +} + +details.data-panel-form-block > summary { + display: flex; + min-height: 2.75rem; + cursor: pointer; + align-items: center; + justify-content: space-between; + padding: 0.65rem 0.75rem; + color: #33424d; + font-size: 0.8rem; + font-weight: 750; + list-style: none; +} + +details.data-panel-form-block > summary::-webkit-details-marker { + display: none; +} + +details.data-panel-form-block > summary::after { + content: '+'; + color: var(--accent-strong); + font-size: 1rem; +} + +details.data-panel-form-block[open] > summary { + border-bottom: 1px solid var(--line); + background: #ffffff; +} + +details.data-panel-form-block[open] > summary::after { + content: '-'; +} + +details.data-panel-form-block > form { + padding: 0.2rem 0.75rem 0.75rem; +} + +.workspace-grid-data > section { + min-width: 0; + align-self: start; +} + +.workspace-grid-data .data-selection-summary { + border-left: 3px solid var(--accent); + background: #f7faf9; +} + +.workspace-grid-data .data-panel-list-block { + border: 0; + border-top: 1px solid var(--line); + border-radius: 0; + padding: 0.75rem 0 0; + background: transparent; +} + +/* AI and review workspaces */ + +.ai-lab-shell { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.ai-lab-shell > .panel-title-row { + order: 0; +} + +.ai-lab-shell > .ai-lab-model-surface[aria-label="Local model asset selection"] { + order: 1; +} + +.ai-lab-shell > .lab-block { + order: 2; +} + +.ai-lab-shell > .ai-lab-state-stack { + order: 3; +} + +.ai-lab-shell > .ai-lab-results-surface { + order: 4; +} + +.ai-lab-shell > .ai-lab-qa-surface { + order: 5; +} + +.ai-lab-shell > .guided-calibration-surface { + order: 6; +} + +.ai-lab-shell > .ai-lab-model-surface { + order: 7; +} + +details.ai-lab-model-surface { + overflow: hidden; + border: 1px solid var(--line); + border-radius: 6px; + padding: 0; + background: #f8fafb; +} + +details.ai-lab-model-surface > summary { + display: flex; + min-height: 2.85rem; + cursor: pointer; + gap: 0.75rem; + align-items: center; + justify-content: space-between; + padding: 0.65rem 0.75rem; + color: #33424d; + font-size: 0.8rem; + font-weight: 750; + list-style: none; +} + +details.ai-lab-model-surface > summary::-webkit-details-marker { + display: none; +} + +details.ai-lab-model-surface > summary::after { + content: '+'; + color: var(--accent-strong); + font-size: 1rem; +} + +details.ai-lab-model-surface[open] > summary::after { + content: '-'; +} + +details.ai-lab-model-surface > summary strong { + margin-left: auto; + color: var(--muted); + font-size: 0.72rem; + font-weight: 650; +} + +.ai-lab-disclosure-body { + border-top: 1px solid var(--line); + padding: 0.75rem; + background: #ffffff; +} + +.ai-lab-shell .lab-block, +.ai-lab-shell .ai-lab-run-surface, +.ai-lab-shell .ai-lab-results-surface, +.ai-lab-shell .ai-lab-qa-surface, +.ai-lab-shell > .ai-lab-model-surface[aria-label="Local model asset selection"] { + border-color: var(--line); + border-radius: 6px; + background: #ffffff; + box-shadow: none; +} + +.change-detection-shell .checkbox-row { + min-width: 0; + align-self: end; + white-space: normal; + overflow-wrap: anywhere; +} + +@media (min-width: 1100px) { + .workspace-grid-data { + grid-template-columns: minmax(17rem, 0.85fr) minmax(18rem, 0.95fr) minmax(21rem, 1.2fr); + align-items: start; + } + + .workspace-grid-data > section, + .workspace-grid-data > section:nth-child(n) { + grid-column: auto; + max-height: calc(100dvh - 10.5rem); + overflow: auto; + } + + .workspace-grid-analysis { + grid-template-columns: minmax(20rem, 0.75fr) minmax(28rem, 1.25fr); + align-items: start; + } + + .workspace-grid-exports { + grid-template-columns: minmax(28rem, 1.2fr) minmax(22rem, 0.8fr); + align-items: start; + } + + .workspace-grid-analysis > section:nth-child(n), + .workspace-grid-ai > section:nth-child(n), + .workspace-grid-exports > section:nth-child(n) { + max-height: calc(100dvh - 10.5rem); + overflow: auto; + align-self: start; + } +} + +/* Overview */ + +.workbench-status-strip { + overflow: hidden; + border: 1px solid var(--line); + border-radius: 7px; + padding: 0; + background: #ffffff; +} + +.status-strip-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1.25rem; + align-items: center; + margin: 0; + border-bottom: 1px solid var(--line); + padding: 1.1rem 1.2rem; + background: #ffffff; +} + +.status-strip-header h2 { + margin: 0; + max-width: 55rem; + font-size: 1.12rem; + line-height: 1.25; +} + +.status-next-action { + max-width: 46rem; + margin: 0.35rem 0 0; + color: var(--muted); + font-size: 0.84rem; + line-height: 1.35; + text-align: left; +} + +.status-readiness { + display: grid; + min-width: 6.5rem; + justify-items: end; +} + +.status-readiness strong { + color: var(--accent-strong); + font-size: 1.65rem; + line-height: 1; +} + +.status-readiness span { + margin-top: 0.25rem; + color: var(--muted); + font-size: 0.72rem; + font-weight: 650; +} + +.status-strip-grid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 0; + padding: 0; +} + +.status-tile { + min-width: 0; + min-height: 6.15rem; + border: 0; + border-right: 1px solid var(--line); + border-radius: 0; + padding: 0.75rem 0.8rem; + background: #ffffff; +} + +.status-tile:last-child { + border-right: 0; +} + +.status-tile-ready { + box-shadow: inset 0 3px 0 #2d9272; +} + +.status-tile-warning { + box-shadow: inset 0 3px 0 #ca7a18; +} + +.status-tile-waiting { + box-shadow: inset 0 3px 0 #aab5bc; +} + +.status-tile strong { + display: -webkit-box; + overflow: hidden; + margin-top: 0.4rem; + color: #23313b; + font-size: 0.87rem; + line-height: 1.25; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.status-tile p { + display: -webkit-box; + overflow: hidden; + margin: 0.2rem 0 0; + color: var(--muted); + font-size: 0.72rem; + line-height: 1.3; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.workflow-guidance-panel { + border: 1px solid var(--line); + border-left: 1px solid var(--line); + padding: 1rem 1.1rem; + background: #ffffff; +} + +.workflow-guidance-panel .panel-title-row { + margin-bottom: 0.8rem; +} + +.workflow-guidance-panel h2 { + font-size: 1rem; +} + +.workflow-guidance-steps { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 0.5rem; +} + +.workflow-guidance-step { + min-width: 0; + min-height: 5rem; + border: 1px solid transparent; + border-radius: 6px; + padding: 0.65rem; + background: #f5f7f8; + text-align: left; +} + +.workflow-guidance-step:hover:not(:disabled) { + border-color: #bdcbd2; + background: #f0f4f5; +} + +.workflow-guidance-step-ready { + background: #f0f7f4; +} + +.workflow-guidance-step-active { + border-color: #91c5ba; + background: var(--accent-soft); +} + +.workflow-guidance-step small { + display: -webkit-box; + overflow: hidden; + margin-top: 0.3rem; + color: var(--muted); + font-size: 0.71rem; + line-height: 1.25; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.overview-actions { + display: flex; + gap: 1rem; + align-items: center; + justify-content: space-between; + border: 1px solid var(--line); + border-left: 1px solid var(--line); + padding: 0.8rem 1rem; + background: #ffffff; +} + +.overview-action-copy h2 { + margin: 0; + font-size: 0.95rem; +} + +.overview-quick-actions { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + align-content: initial; +} + +.quick-action-button { + width: auto; + min-height: 2.25rem; + padding: 0.4rem 0.72rem; + font-size: 0.78rem; +} + +/* Map-first workbench */ + +.map-workspace-shell { + display: grid; + gap: 0.85rem; + border: 0; + padding: 0; + background: transparent; +} + +.map-workspace-shell > .panel-title-row { + margin: 0; + border: 0; + padding: 0; +} + +.map-workspace-shell > .panel-title-row h2 { + font-size: 1.08rem; +} + +.map-control-surface { + order: 1; + border: 1px solid var(--line); + padding: 0.75rem; + background: #ffffff; +} + +.map-workspace-shell .map-toolbar { + display: grid; + grid-template-columns: minmax(13rem, 1.3fr) minmax(10rem, 0.9fr) minmax(8rem, 0.65fr) minmax(8rem, 0.65fr) minmax(10rem, 0.9fr); + gap: 0.6rem; + align-items: end; +} + +.map-workspace-shell .map-toolbar label, +.map-workspace-shell .map-toolbar .layer-control-card { + margin: 0; +} + +.layer-control-card { + min-height: 3.9rem; + border: 1px solid var(--line); + border-radius: 6px; + padding: 0.45rem 0.55rem; + background: #f8fafb; +} + +.map-status { + display: grid; + min-height: 3.9rem; + align-content: center; + border-left: 3px solid var(--accent); + border-radius: 4px; + padding: 0.45rem 0.65rem; + background: #f3f8f7; +} + +.map-status strong { + font-size: 0.8rem; +} + +.map-status span { + display: none; +} + +.basemap-policy-notice { + margin-bottom: 0.65rem; + border: 0; + border-radius: 5px; + padding: 0.45rem 0.6rem; + background: #f3f6f8; +} + +.basemap-policy-notice strong, +.basemap-policy-notice span { + font-size: 0.72rem; +} + +.map-frame-surface { + order: 2; + overflow: hidden; + border: 1px solid #aebac2; + border-radius: 7px; + padding: 0; + background: #e9eff1; + box-shadow: 0 7px 22px rgba(29, 43, 54, 0.09); +} + +.map-frame-surface .map-container, +.workbench-main .map-container { + height: clamp(30rem, 60vh, 52rem); + min-height: 30rem; + border: 0; + border-radius: 0; +} + +.map-layer-details, +.map-advanced-tools { + overflow: hidden; + border: 1px solid var(--line); + border-radius: 7px; + background: #ffffff; +} + +.map-layer-details { + order: 3; +} + +.qa-evidence-map-status { + order: 4; +} + +.map-empty-state { + order: 5; +} + +.map-layer-details > summary, +.map-advanced-tools > summary { + display: flex; + min-height: 3rem; + cursor: pointer; + gap: 1rem; + align-items: center; + justify-content: space-between; + padding: 0.7rem 0.85rem; + color: #263540; + font-size: 0.82rem; + font-weight: 750; + list-style: none; +} + +.map-layer-details > summary::-webkit-details-marker, +.map-advanced-tools > summary::-webkit-details-marker { + display: none; +} + +.map-layer-details > summary::after, +.map-advanced-tools > summary::after { + content: '+'; + color: var(--accent-strong); + font-size: 1rem; +} + +.map-layer-details[open] > summary::after, +.map-advanced-tools[open] > summary::after { + content: '-'; +} + +.map-layer-details > summary strong, +.map-advanced-tools > summary strong { + margin-left: auto; + color: var(--muted); + font-size: 0.74rem; + font-weight: 600; +} + +.map-context-summary, +.layer-provenance-rail { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0; + border-top: 1px solid var(--line); + padding: 0; + background: transparent; +} + +.map-context-summary > div, +.layer-provenance-rail > div { + min-width: 0; + border: 0; + border-right: 1px solid var(--line); + border-radius: 0; + padding: 0.65rem 0.75rem; + background: transparent; +} + +.map-context-summary > div:last-child, +.layer-provenance-rail > div:last-child { + border-right: 0; +} + +.map-context-summary span, +.layer-provenance-rail span { + font-size: 0.64rem; +} + +.map-context-summary strong, +.layer-provenance-rail strong { + font-size: 0.78rem; + line-height: 1.25; +} + +.map-context-summary small { + font-size: 0.69rem; +} + +.map-empty-state { + margin: 0; + border: 1px dashed #afbdc5; + background: #f8fafb; +} + +.map-inspection-surface { + order: 6; + display: grid; + grid-template-columns: 1fr; + gap: 0.85rem; + border: 0; + padding: 0; + background: transparent; +} + +.gis-test-run-surface { + border: 1px solid var(--line); + border-radius: 7px; + padding: 1rem; + background: #ffffff; +} + +.gis-test-run-grid, +.guided-gis-steps { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.map-advanced-tools-body { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0; + border-top: 1px solid var(--line); +} + +.map-advanced-tools-body > div { + border: 0; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + border-radius: 0; + background: #ffffff; +} + +.map-advanced-tools-body > div:nth-child(2n) { + border-right: 0; +} + +.qa-evidence-map-status { + border: 1px solid var(--line); + border-radius: 7px; + background: #ffffff; +} + +/* Responsive shell */ + +@media (min-width: 1800px) { + .workbench-layout { + grid-template-columns: 13.25rem minmax(0, 1fr); + } + + .workbench-main { + padding: 1.6rem 2rem 2.5rem; + } + + .workspace-grid-data { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .workspace-grid-analysis, + .workspace-grid-ai, + .workspace-grid-exports { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .map-frame-surface .map-container, + .workbench-main .map-container { + height: clamp(38rem, 68vh, 58rem); + min-height: 38rem; + } +} + +@media (max-width: 1360px) { + .workbench-layout { + grid-template-columns: 10.75rem minmax(0, 1fr); + height: calc(100dvh - 4.5rem); + } + + .workbench-inspector { + position: fixed; + width: min(27rem, 100vw); + max-height: none; + } + +} + +@media (max-width: 920px) { + .app-shell > header.workbench-topbar { + position: relative; + display: grid; + grid-template-columns: 1fr; + gap: 0.65rem; + min-height: auto; + padding: 0.7rem 0.85rem; + } + + .context-bar { + display: flex; + overflow-x: auto; + scroll-snap-type: x proximity; + } + + .context-bar > div { + min-width: 10rem; + flex: 1 0 10rem; + scroll-snap-align: start; + } + + .workbench-layout { + display: block; + height: auto; + min-height: 0; + } + + .workbench-sidebar { + position: relative; + top: auto; + z-index: 15; + overflow-x: auto; + border: 0; + border-bottom: 1px solid var(--line); + padding: 0.45rem 0.65rem; + background: #ffffff; + } + + .workbench-sidebar nav { + display: flex; + width: max-content; + min-width: 100%; + gap: 0.35rem; + } + + .nav-group { + display: contents; + } + + .nav-section-label { + display: none; + } + + .workbench-sidebar .nav-item { + width: auto; + min-width: 6.25rem; + min-height: 2.5rem; + flex: 0 0 auto; + padding: 0.45rem 0.62rem; + text-align: center; + } + + .nav-item-active { + box-shadow: inset 0 -3px 0 var(--accent); + } + + .nav-item-active small { + display: none; + } + + .workbench-main { + overflow: visible; + padding: 1rem; + } + + .workbench-inspector { + top: 0; + width: 100vw; + } + + .workspace-grid-data > section, + .workspace-grid-data > section:nth-child(n) { + max-height: none; + overflow: visible; + } + + .workspace-grid-analysis > section:nth-child(n), + .workspace-grid-ai > section:nth-child(n), + .workspace-grid-exports > section:nth-child(n) { + max-height: none; + overflow: visible; + } + + .workflow-guidance-steps { + grid-template-columns: repeat(5, minmax(9.5rem, 1fr)); + overflow-x: auto; + scroll-snap-type: x proximity; + } + + .workflow-guidance-step { + scroll-snap-align: start; + } + + .overview-actions { + align-items: flex-start; + } + + .map-context-summary, + .layer-provenance-rail, + .gis-test-run-grid, + .guided-gis-steps { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .map-advanced-tools-body { + grid-template-columns: 1fr; + } + + .map-workspace-shell .map-toolbar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .map-status { + grid-column: 1 / -1; + } + + .map-advanced-tools-body > div, + .map-advanced-tools-body > div:nth-child(2n) { + border-right: 0; + } +} + +@media (max-width: 620px) { + .brand-block h1 { + font-size: 1rem; + } + + .context-bar > div { + min-width: 8.75rem; + flex-basis: 8.75rem; + } + + .workbench-main { + padding: 0.8rem 0.75rem 1.5rem; + } + + .workspace-heading { + display: flex; + align-items: flex-end; + margin-bottom: 0.8rem; + padding-bottom: 0.7rem; + } + + .workspace-heading h2 { + font-size: 1.3rem; + } + + .workspace-heading-actions { + display: grid; + justify-items: end; + } + + .workspace-heading-actions > p { + display: none; + } + + .status-strip-header { + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.6rem; + padding: 0.9rem; + } + + .status-strip-header h2 { + font-size: 1rem; + } + + .status-readiness strong { + font-size: 1.3rem; + } + + .status-strip-grid { + grid-auto-flow: column; + grid-template-columns: none; + grid-auto-columns: minmax(9rem, 1fr); + overflow-x: auto; + scroll-snap-type: x proximity; + } + + .status-tile, + .status-tile:nth-child(3), + .status-tile:nth-child(-n + 3) { + min-height: 6.3rem; + border-right: 1px solid var(--line); + border-bottom: 0; + scroll-snap-align: start; + } + + .status-tile:last-child { + border-right: 0; + } + + .overview-actions { + display: grid; + gap: 0.7rem; + } + + .overview-quick-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + width: 100%; + } + + .quick-action-button { + width: 100%; + } + + .map-workspace-shell .map-toolbar, + .map-context-summary, + .layer-provenance-rail, + .gis-test-run-grid, + .guided-gis-steps, + .guided-gis-actions { + grid-template-columns: 1fr; + } + + .map-status { + grid-column: auto; + } + + .map-frame-surface .map-container, + .workbench-main .map-container { + height: 28rem; + min-height: 28rem; + } + + .map-layer-details > summary, + .map-advanced-tools > summary { + align-items: flex-start; + } + + .map-layer-details > summary strong, + .map-advanced-tools > summary strong { + display: none; + } +} + +@media (min-width: 621px) { + .status-strip-grid { + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 0; + } + + .status-tile, + .status-tile:nth-child(3), + .status-tile:nth-child(-n + 3) { + border-right: 1px solid var(--line); + border-bottom: 0; + } + + .status-tile:last-child { + border-right: 0; + } + + .workflow-guidance-steps { + gap: 0.5rem; + } + + .workflow-guidance-steps .workflow-guidance-step { + border-color: transparent; + background: #f5f7f8; + } + + .workflow-guidance-steps .workflow-guidance-step-ready { + background: #f0f7f4; + } + + .workflow-guidance-steps .workflow-guidance-step-active { + border-color: #91c5ba; + background: var(--accent-soft); + } +}