Add interactive GeoIntel storytelling and portfolio assets
@@ -1,314 +1,197 @@
|
|||||||
# GeoIntel Belgium and the Belgian North Sea
|
# GeoIntel
|
||||||
|
|
||||||
GeoIntel is a map-first GeoAI Workbench for Belgium and the Belgian North Sea.
|
<p align="center">
|
||||||
It combines governed official-source coverage, raster/vector processing,
|
<img src="frontend/public/geointel-icon.png" width="92" alt="GeoIntel logo">
|
||||||
historical comparison, computer vision, QA/QC and geospatial exports.
|
</p>
|
||||||
|
|
||||||
Mol and the Kempen remain deep regression and model-validation references. The
|
<p align="center"><strong>Evidence-first GeoAI voor België en de Belgische Noordzee.</strong></p>
|
||||||
release scope is all of Belgium plus legally labelled Belgian maritime zones;
|
|
||||||
source coverage remains explicit per theme and jurisdiction.
|
|
||||||
|
|
||||||
GeoIntel is not a generic dashboard or chatbot. The core product is:
|
GeoIntel is een kaartgerichte GeoAI-workbench waarin officiële databronnen,
|
||||||
|
ruimtelijke analyse, PyTorch-computervisie, QA/QC en export samenkomen. Het
|
||||||
|
platform bewaart niet alleen een resultaat, maar ook de bron, ruimtelijke
|
||||||
|
context, modelversie en het bewijs waarmee dat resultaat gecontroleerd kan
|
||||||
|
worden.
|
||||||
|
|
||||||
> data → processing → geospatial output → QA/QC → export
|
> **Data → ruimtelijke verwerking → AI-resultaat → kwaliteitsbewijs → export**
|
||||||
|
|
||||||
## Current milestone
|

|
||||||
|
|
||||||
**v1.0.0 - Belgium/North Sea release**
|
## Waarom GeoIntel?
|
||||||
|
|
||||||
The canonical release controls are:
|
Veel geoportalen tonen lagen. GeoIntel ondersteunt een volledige,
|
||||||
|
controleerbare analyseketen:
|
||||||
|
|
||||||
- `docs/00-start/START_HERE.md`
|
- selecteer een officiële grens of teken een eigen Area of Interest;
|
||||||
- `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md`
|
- combineer vector-, raster- en historische bronnen met expliciete dekking;
|
||||||
- `docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md`
|
- voer GIS- en CUDA-versnelde beeldanalyse uit;
|
||||||
- `docs/RELEASE_RUNBOOK.md`
|
- vergelijk kandidaatresultaten met referentiedata;
|
||||||
- `docs/KNOWN_LIMITATIONS.md`
|
- inspecteer false positives, false negatives, overlap en provenance;
|
||||||
- `docs/DEFINITION_OF_DONE.md`
|
- exporteer alleen wat als reproduceerbaar resultaat is vastgelegd.
|
||||||
|
|
||||||
Older milestone and sprint handoff files remain historical evidence. They do
|
Mol en de Kempen blijven de gouden regressiegebieden. De productgrens is heel
|
||||||
not override the active national/maritime scope freeze or RC roadmap.
|
België plus de juridisch benoemde Belgische maritieme zones.
|
||||||
|
|
||||||
## Core V1 vertical slice
|
## Product in beeld
|
||||||
|
|
||||||
The first implementation target is:
|
### Interactieve projectketen
|
||||||
|
|
||||||
1. Project + Area creation.
|
De landingspagina vertaalt de technische keten naar vier interactieve
|
||||||
2. Dataset registration/upload and metadata extraction.
|
schakels. De illustratie reageert op selectie, bronnen, analyse en bewijs en
|
||||||
3. Reference building layer loading.
|
respecteert automatisch `prefers-reduced-motion`.
|
||||||
4. Predicted detection layer loading/import.
|
|
||||||
5. QA/QC matching against reference polygons.
|
|
||||||
6. Metrics and false positive/false negative outputs.
|
|
||||||
7. GeoJSON export.
|
|
||||||
8. Minimal map/workbench UI.
|
|
||||||
|
|
||||||
## Primary stack
|

|
||||||
|
|
||||||
- Frontend: React, TypeScript, MapLibre GL, Deck.gl, Tailwind.
|
### Kaart als werkruimte
|
||||||
- Backend: FastAPI, Python.
|
|
||||||
- Database: PostgreSQL + PostGIS.
|
|
||||||
- GIS processing: GeoPandas, Shapely, Rasterio, PyProj, GDAL.
|
|
||||||
- AI: PyTorch, Ultralytics YOLO, SAM-compatible architecture.
|
|
||||||
- Jobs: Redis + RQ.
|
|
||||||
- Storage: local filesystem first, MinIO-compatible later.
|
|
||||||
|
|
||||||
## Codex instructions
|
De kaart blijft het primaire werkblad. Thema, broncontext, selectie en
|
||||||
|
resultaten zijn vanuit dezelfde ruimtelijke context bereikbaar.
|
||||||
|
|
||||||
Codex must start with:
|

|
||||||
|
|
||||||
1. `docs/00-start/START_HERE.md`
|
### Kwaliteit vóór resultaat
|
||||||
2. `prompts/codex/M11_ARCHITECT_MASTER_PROMPT.md`
|
|
||||||
|
|
||||||
Then follow the build order in:
|
QA/QC is een eerste-klas workflow. Bewaarde controles koppelen scores aan
|
||||||
|
objectbewijs, kandidaat- en referentielagen en technische provenance.
|
||||||
|
|
||||||
- `docs/build/BUILD_ORDER_DEPENDENCY_GRAPH.md`
|

|
||||||
- `docs/build/CODEX_OPERATING_SYSTEM.md`
|
|
||||||
|
|
||||||
Before every implementation pass, run available preflight/smoke scripts where applicable.
|
## Kernmogelijkheden
|
||||||
|
|
||||||
## Repo principle
|
| Domein | Mogelijkheden |
|
||||||
|
| --- | --- |
|
||||||
|
| Werkgebieden | Officiële grenzen, vrije AOI-selectie, clipping en buffering |
|
||||||
|
| Bronnen | GeoJSON, vector, raster, orthofoto, historische en regionale catalogi |
|
||||||
|
| GIS | CRS-normalisatie, spatial joins, rasterstatistiek, tile- en selectieanalyse |
|
||||||
|
| GeoAI | PyTorch, Ultralytics YOLO en SAM-compatibele abstrahering |
|
||||||
|
| Tijd | Immutable DatasetVersions en vergelijking van ruimtelijke snapshots |
|
||||||
|
| QA/QC | Precision, recall, F1, IoU, false-positive/-negative evidence en reviews |
|
||||||
|
| Export | Reproduceerbare GeoJSON- en artefactexports met provenance |
|
||||||
|
| Runtime | DockerMan-native Unraid-container met PostGIS, Redis, backend en frontend |
|
||||||
|
|
||||||
This is a documentation-driven engineering repo. The documentation is not decorative; it is the control system for autonomous implementation.
|
## Architectuur
|
||||||
|
|
||||||
## Fastest Day 1 command path
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
```bash
|
UI["React + MapLibre workbench"] --> API["FastAPI contracts"]
|
||||||
make readiness
|
API --> GIS["GeoPandas / Shapely / Rasterio"]
|
||||||
|
API --> JOBS["Redis + RQ"]
|
||||||
|
JOBS --> AI["PyTorch / YOLO / SAM"]
|
||||||
|
GIS --> DB[("PostgreSQL + PostGIS")]
|
||||||
|
AI --> DB
|
||||||
|
DB --> QA["QA/QC + provenance"]
|
||||||
|
QA --> UI
|
||||||
|
QA --> EXPORT["GeoJSON / artifacts"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Unraid / Tower deployment
|
De publieke endpoints volgen [docs/API_CONTRACTS.md](docs/API_CONTRACTS.md).
|
||||||
|
PostGIS en de persistentieregels volgen
|
||||||
|
[docs/DATABASE_IMPLEMENTATION_PLAN.md](docs/DATABASE_IMPLEMENTATION_PLAN.md).
|
||||||
|
|
||||||
GeoIntel runs on Unraid as an all-in-one DockerMan-native container. The container embeds PostGIS, runs the FastAPI backend internally, and serves the frontend through nginx on one editable web port.
|
## Stack
|
||||||
|
|
||||||
Unraid template assets live in:
|
- **Frontend:** React 18, TypeScript, MapLibre GL, Lucide en Vite
|
||||||
|
- **Backend:** FastAPI en Python
|
||||||
|
- **Spatial:** GeoPandas, Shapely, Rasterio, PyProj en GDAL
|
||||||
|
- **AI:** PyTorch, Ultralytics YOLO en SAM-compatibele segmentatie
|
||||||
|
- **Data:** PostgreSQL/PostGIS, lokale artefactopslag en immutable versions
|
||||||
|
- **Jobs:** Redis en RQ
|
||||||
|
- **Deployment:** één DockerMan-native Unraid-container
|
||||||
|
|
||||||
- `deploy/unraid/geointel.env.example`
|
## Lokaal starten
|
||||||
- `deploy/unraid/geointel-unraid-template.xml`
|
|
||||||
- `deploy/unraid/geointel-icon.svg`
|
|
||||||
- `deploy/unraid/geointel-icon.png`
|
|
||||||
- `docker-compose.unraid.yml`
|
|
||||||
|
|
||||||
Copy the Unraid env template to `.env` in the checkout and edit ports/paths there:
|
Vereisten: Python 3.11+, Node 20.19+ of 22.12+, PostgreSQL/PostGIS en Redis.
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/user/appdata/geointel
|
|
||||||
cp deploy/unraid/geointel.env.example .env
|
|
||||||
nano .env
|
|
||||||
docker build -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
|
|
||||||
bash deploy/unraid/run-dockerman-container.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Common editable values:
|
|
||||||
|
|
||||||
```env
|
|
||||||
GEOINTEL_FRONTEND_PORT=1202
|
|
||||||
GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
|
|
||||||
GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data
|
|
||||||
```
|
|
||||||
|
|
||||||
### Guest demonstration access
|
|
||||||
|
|
||||||
Guest access is enabled by default whenever the operator login gate is active.
|
|
||||||
No additional guest toggle is required for a new authenticated installation:
|
|
||||||
|
|
||||||
```env
|
|
||||||
GEOINTEL_AUTH_ENABLED=true
|
|
||||||
GEOINTEL_AUTH_USERNAME=operator
|
|
||||||
GEOINTEL_AUTH_PASSWORD_HASH=pbkdf2_sha256$...
|
|
||||||
GEOINTEL_AUTH_SESSION_SECRET=<independent-random-secret-of-at-least-32-characters>
|
|
||||||
GEOINTEL_GUEST_ACCESS_ENABLED=true
|
|
||||||
GEOINTEL_GUEST_DISPLAY_NAME=Gast
|
|
||||||
GEOINTEL_GUEST_SESSION_TTL_SECONDS=7200
|
|
||||||
```
|
|
||||||
|
|
||||||
The login page offers **Als gast verkennen**. A guest receives a short-lived,
|
|
||||||
read-only session scoped to the seeded demo project and sees only the map and
|
|
||||||
existing quality evidence. Set `GEOINTEL_GUEST_ACCESS_ENABLED=false` to hide and
|
|
||||||
disable this route. This is not multi-user authorization or tenant isolation;
|
|
||||||
use a separate demo instance when the installation contains private or
|
|
||||||
operational datasets.
|
|
||||||
|
|
||||||
The backend and PostGIS ports are intentionally not exposed to the LAN in the all-in-one runtime. See `deploy/unraid/README.md` for full setup, port-change and cleanup notes.
|
|
||||||
|
|
||||||
On Tower/Unraid, `scripts/deploy_tower.ps1` and `scripts/deploy_tower.sh` validate the Compose reference but build with plain `docker build`, then automatically install the editable DockerMan template as `/boot/config/plugins/dockerMan/templates-user/my-geointel.xml`, install the PNG icon as `/boot/config/plugins/dockerMan/images/geointel-icon.png`, remove any old Compose-owned `geointel` container and start the final container with DockerMan labels.
|
|
||||||
|
|
||||||
## Sprint 2 quick start
|
|
||||||
|
|
||||||
- Update dependencies:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pip install -e backend/.[dev]
|
python -m pip install -e backend/.[dev]
|
||||||
cd frontend && npm install
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
- Run full readiness checks (with no scope expansion):
|
Voor de volledige lokale stack:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Open daarna `http://localhost:1202`.
|
||||||
|
|
||||||
|
### Gastdemo
|
||||||
|
|
||||||
|
Met `GEOINTEL_GUEST_ACCESS_ENABLED=true` biedt de toegangspagina een
|
||||||
|
kortlevende, alleen-lezen demosessie. Die toont uitsluitend het ingestelde
|
||||||
|
demoproject en bewaard kwaliteitsbewijs; dit is geen multi-user- of
|
||||||
|
tenantisolatie.
|
||||||
|
|
||||||
|
## NVIDIA/CUDA
|
||||||
|
|
||||||
|
Productie-AI gebruikt de NVIDIA GPU van de server. De runtime faalt gesloten
|
||||||
|
wanneer CUDA vereist maar niet beschikbaar is; ontbrekende modelconfiguratie
|
||||||
|
wordt als `not_configured` getoond en nooit als geslaagde inferentie.
|
||||||
|
|
||||||
|
Controleer GPU-zichtbaarheid in de container met:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec geointel nvidia-smi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kwaliteitscontrole
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
python -m compileall backend/app
|
python -m compileall backend/app
|
||||||
cd backend && python -m pytest
|
cd backend && python -m pytest
|
||||||
cd ../frontend && npm run typecheck && npm run build
|
|
||||||
bash scripts/run_readiness_check.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
- Raster workflow validation command (backend only):
|
# Frontend
|
||||||
|
cd frontend
|
||||||
```bash
|
|
||||||
bash scripts/smoke_backend_import.sh
|
|
||||||
cd backend && python -c "from app.main import app; print(app.title)"
|
|
||||||
```
|
|
||||||
|
|
||||||
If `rasterio` is not installed, raster metadata endpoints return `RASTER_PROCESSING_UNAVAILABLE` and the frontend displays the
|
|
||||||
state as failed until the dependency is added.
|
|
||||||
|
|
||||||
## Sprint 4 raster foundation
|
|
||||||
|
|
||||||
- Raster operations now support:
|
|
||||||
- raster metadata extraction,
|
|
||||||
- raster preview generation,
|
|
||||||
- raster clip by area (with provenance on derived datasets),
|
|
||||||
- raster tile generation with manifest output.
|
|
||||||
- Raster services are dependency-aware:
|
|
||||||
- if `rasterio` is unavailable, endpoints return `RASTER_PROCESSING_UNAVAILABLE`.
|
|
||||||
- if preview dependencies (`numpy`, `pillow`) are unavailable, preview generation is unavailable with a clear error.
|
|
||||||
- Enable raster stack explicitly when needed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend && python -m pip install -e .[dev,raster]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sprint 5 raster analytics hardening
|
|
||||||
|
|
||||||
- Added raster band statistics (min/max/mean/std, nodata ratio/count, valid pixel count, dtype, optional histograms).
|
|
||||||
- Added raster reproject workflow with CRS validation and provenance persistence.
|
|
||||||
- Extended tile manifest expectations (`tile_set_id`, `tile_size`, `overlap`, `bounds`, `source_raster_id`, `tile_paths`, `tile_server`).
|
|
||||||
- Clarified raster operation availability in frontend/backend docs (`RASTER_PROCESSING_UNAVAILABLE` and invalid-CRS cases).
|
|
||||||
|
|
||||||
- Raster workflow command set (where available):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
python -m pip install -e .[dev,raster]
|
|
||||||
python -m pytest
|
|
||||||
cd ../frontend
|
|
||||||
npm run typecheck
|
npm run typecheck
|
||||||
|
npm run test:unit
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then give Codex the prompt in:
|
Een wijziging is pas afgerond wanneer de toepasselijke criteria uit
|
||||||
|
[docs/DEFINITION_OF_DONE.md](docs/DEFINITION_OF_DONE.md) aantoonbaar zijn
|
||||||
|
afgevinkt.
|
||||||
|
|
||||||
- `prompts/codex/final/DAY_1_MASTER_PROMPT.md`
|
## Tower / Unraid deploy
|
||||||
|
|
||||||
|
De canonieke checkout op Tower staat in `/mnt/user/appdata/geointel`. Gebruik
|
||||||
|
de releasewrapper; de productiecontainer wordt niet met een alternatieve
|
||||||
|
Compose-checkout vervangen.
|
||||||
|
|
||||||
## M13 Codex optimization
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts/deploy_tower.ps1 `
|
||||||
For the first serious Codex build run, use:
|
-RemoteHost root@<tower-ip> `
|
||||||
|
-RemotePath /mnt/user/appdata/geointel `
|
||||||
- `prompts/codex/m13/DAY_1_OPTIMIZED_MASTER_PROMPT.md`
|
-RemoteBranch main `
|
||||||
|
-FrontendUrl http://<tower-ip>:1202
|
||||||
Codex should also use the relevant reusable skill under `skills/` for each implementation pass. Validate the optimization assets with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make m13
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The full readiness path remains:
|
Zie [deploy/unraid/README.md](deploy/unraid/README.md) en
|
||||||
|
[docs/RELEASE_RUNBOOK.md](docs/RELEASE_RUNBOOK.md) voor configuratie,
|
||||||
|
healthchecks en rollback.
|
||||||
|
|
||||||
```bash
|
## Documentatiekaart
|
||||||
make readiness
|
|
||||||
```
|
|
||||||
|
|
||||||
|
- [Start hier](docs/00-start/START_HERE.md)
|
||||||
|
- [Belgium/North Sea scope freeze](docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md)
|
||||||
|
- [API-contracten](docs/API_CONTRACTS.md)
|
||||||
|
- [Database-implementatieplan](docs/DATABASE_IMPLEMENTATION_PLAN.md)
|
||||||
|
- [Bekende beperkingen](docs/KNOWN_LIMITATIONS.md)
|
||||||
|
- [Release-runbook](docs/RELEASE_RUNBOOK.md)
|
||||||
|
|
||||||
## M14 Build Launch
|
## Portfolio-assets
|
||||||
|
|
||||||
For the first serious implementation run, use:
|
De map [`docs/assets/portfolio`](docs/assets/portfolio) bevat presentatieklare
|
||||||
|
screenshots van de landingspagina, interactieve illustratie, kaartwerkruimte,
|
||||||
|
kwaliteitscontrole en mobiele weergave. De twee originele campagnebeelden
|
||||||
|
staan in [`frontend/public/portfolio`](frontend/public/portfolio) en worden
|
||||||
|
ook door de applicatie gebruikt.
|
||||||
|
|
||||||
- `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md`
|

|
||||||
- `docs/40-build-launch/BUILD_SUCCESS_DEFINITION.md`
|
|
||||||
- `docs/40-build-launch/CODEX_STOP_RULES.md`
|
|
||||||
- `prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md`
|
|
||||||
|
|
||||||
Validate launch assets with:
|
## Status
|
||||||
|
|
||||||
```bash
|
Actieve mijlpaal: **v1.0.0 — Belgium and Belgian North Sea**.
|
||||||
make m14
|
|
||||||
```
|
|
||||||
|
|
||||||
Full readiness remains:
|
GeoIntel is een project van Jens / ITWorx.tech.
|
||||||
|
|
||||||
```bash
|
|
||||||
make readiness
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sprint 1 execution (Sprint 1 only)
|
|
||||||
|
|
||||||
From a clean machine:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend && python -m pip install -e .[dev]
|
|
||||||
cd ..
|
|
||||||
make backend-install
|
|
||||||
make frontend-install
|
|
||||||
make readiness
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy `.env.example` to `.env` only when you want local overrides. Docker Compose has safe defaults for the local PostGIS/backend/frontend stack and does not require a root `.env` file to exist.
|
|
||||||
|
|
||||||
With Docker Compose, open the workbench at `http://localhost:1202`.
|
|
||||||
|
|
||||||
The Docker frontend is served by nginx and proxies `/api` and `/health` to the backend container, so browser clients should use the frontend URL only, for example `http://192.168.10.150:1202` on a LAN host.
|
|
||||||
|
|
||||||
Runtime containers include healthchecks for PostGIS, backend and frontend. After
|
|
||||||
startup, inspect them with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify the browser-facing API proxy after rebuilding Docker images:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/verify_browser_runtime.sh http://localhost:1202 http://localhost:8000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify the Docker GIS runtime after rebuilding the backend image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/verify_gis_runtime.sh http://localhost:1202
|
|
||||||
```
|
|
||||||
|
|
||||||
On the LAN host use the published browser URL, for example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/verify_gis_runtime.sh http://192.168.10.150:1202
|
|
||||||
```
|
|
||||||
|
|
||||||
Load the explicit offline demo workflow:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://192.168.10.150:1202/api/v1/demo/workflow
|
|
||||||
```
|
|
||||||
|
|
||||||
If `/api/v1/projects` returns frontend HTML instead of a JSON envelope, rebuild
|
|
||||||
and restart the frontend container.
|
|
||||||
|
|
||||||
Useful direct verification commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m compileall backend/app
|
|
||||||
cd backend && python -c "from app.main import app; print(app.title)"
|
|
||||||
python -m pytest
|
|
||||||
cd ../frontend && npm run typecheck
|
|
||||||
cd ../frontend && npm run build
|
|
||||||
docker compose config
|
|
||||||
bash scripts/run_readiness_check.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
If `make` or `docker` are unavailable in your shell, run the equivalent script entrypoints directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/backend_install.sh
|
|
||||||
bash scripts/backend_test.sh
|
|
||||||
bash scripts/frontend_install.sh
|
|
||||||
bash scripts/frontend_typecheck.sh
|
|
||||||
bash scripts/frontend_build.sh
|
|
||||||
bash scripts/run_readiness_check.sh
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -11679,3 +11679,28 @@ Deployment evidence:
|
|||||||
- Repeated the browser-rendered landing, guest-session, map and quality checks
|
- Repeated the browser-rendered landing, guest-session, map and quality checks
|
||||||
against `http://192.168.10.150:1202`; the inspected live flow produced no
|
against `http://192.168.10.150:1202`; the inspected live flow produced no
|
||||||
browser-console errors.
|
browser-console errors.
|
||||||
|
|
||||||
|
## 2026-07-28 - Interactive presentation and portfolio pass
|
||||||
|
|
||||||
|
- Added a keyboard-operable four-stage project-chain illustration to the
|
||||||
|
public landing page. Selection, source governance, PyTorch analysis and QA
|
||||||
|
evidence each change the explanatory state and map treatment.
|
||||||
|
- Added restrained route, scan, evidence and map-depth motion. Every animation
|
||||||
|
and transition is disabled under `prefers-reduced-motion`.
|
||||||
|
- Generated two original, text-free GeoIntel campaign images: a Belgian
|
||||||
|
land/sea GeoAI hero and a building-detection/QA orthophoto composition. The
|
||||||
|
assets are stored in `frontend/public/portfolio` for reuse.
|
||||||
|
- Captured repository-owned portfolio screenshots for the landing hero,
|
||||||
|
interactive project story, map workbench, quality workbench and mobile
|
||||||
|
landing experience in `docs/assets/portfolio`.
|
||||||
|
- Replaced the legacy README with a product-led overview, visual tour,
|
||||||
|
architecture diagram, governed AI/CUDA behavior, setup, verification,
|
||||||
|
deployment and documentation map.
|
||||||
|
- Browser checks confirmed the tab interaction, the PyTorch stage content and
|
||||||
|
a 390 px layout without horizontal overflow.
|
||||||
|
|
||||||
|
Verified in this pass:
|
||||||
|
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run test:unit -- --maxWorkers=1 --reporter=verbose` (`45 passed`)
|
||||||
|
- `npm run build` (`1892 modules transformed`, production build passed)
|
||||||
|
|||||||
@@ -980,3 +980,12 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Make guest authentication independent of optional browser scrolling support.
|
- [x] Make guest authentication independent of optional browser scrolling support.
|
||||||
- [x] Restore the explicit frontend dependency exclusion required by the Unraid build-context gate.
|
- [x] Restore the explicit frontend dependency exclusion required by the Unraid build-context gate.
|
||||||
- [x] Push the verified release and redeploy the Dockerman-native all-in-one runtime from `/mnt/user/appdata/geointel`.
|
- [x] Push the verified release and redeploy the Dockerman-native all-in-one runtime from `/mnt/user/appdata/geointel`.
|
||||||
|
|
||||||
|
# Sprint 230 - Interactive storytelling and portfolio presentation
|
||||||
|
|
||||||
|
- [x] Add an accessible interactive project-chain illustration to the public landing page.
|
||||||
|
- [x] Add restrained map, scan, route and evidence motion with a complete reduced-motion fallback.
|
||||||
|
- [x] Produce original GeoIntel hero and building-QA campaign imagery for project and portfolio use.
|
||||||
|
- [x] Capture portfolio-ready landing, story, map, quality and mobile screenshots.
|
||||||
|
- [x] Rewrite the repository README around product value, architecture, operation and visual evidence.
|
||||||
|
- [ ] Deploy and visually verify the presentation release on the Tower runtime.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
@@ -22,6 +22,7 @@ import { formatAuthError } from '../../lib/authError'
|
|||||||
import '../../styles/landing.css'
|
import '../../styles/landing.css'
|
||||||
import { GeoIntelMark } from '../brand/GeoIntelBrand'
|
import { GeoIntelMark } from '../brand/GeoIntelBrand'
|
||||||
import { ItWorxSignature } from '../brand/ItWorxSignature'
|
import { ItWorxSignature } from '../brand/ItWorxSignature'
|
||||||
|
import { LandingProjectStory } from './LandingProjectStory'
|
||||||
|
|
||||||
interface LandingPageProps {
|
interface LandingPageProps {
|
||||||
onAuthenticated: (session: AuthSession) => void
|
onAuthenticated: (session: AuthSession) => void
|
||||||
@@ -338,6 +339,8 @@ export function LandingPage({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<LandingProjectStory />
|
||||||
|
|
||||||
<section className="landing-workflow" id="werkproces" aria-labelledby="workflow-title">
|
<section className="landing-workflow" id="werkproces" aria-labelledby="workflow-title">
|
||||||
<div className="landing-workflow-visual">
|
<div className="landing-workflow-visual">
|
||||||
<div className="landing-workflow-map-image" aria-hidden="true" />
|
<div className="landing-workflow-map-image" aria-hidden="true" />
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
import { LandingProjectStory } from './LandingProjectStory'
|
||||||
|
|
||||||
|
afterEach(cleanup)
|
||||||
|
|
||||||
|
describe('LandingProjectStory', () => {
|
||||||
|
it('moves through the evidence-first project chain', () => {
|
||||||
|
render(<LandingProjectStory />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('tabpanel').textContent).toContain('Teken één ruimtelijke vraag')
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('tab', { name: '03 Analyseer' }))
|
||||||
|
|
||||||
|
expect(screen.getByRole('tab', { name: '03 Analyseer' }).getAttribute('aria-selected')).toBe('true')
|
||||||
|
expect(screen.getByRole('tabpanel').textContent).toContain('Laat GIS en PyTorch samenwerken')
|
||||||
|
expect(screen.getByRole('tabpanel').textContent).toContain('186')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('supports arrow-key navigation between stages', () => {
|
||||||
|
render(<LandingProjectStory />)
|
||||||
|
|
||||||
|
fireEvent.keyDown(screen.getByRole('tab', { name: '01 Selecteer' }), { key: 'ArrowRight' })
|
||||||
|
|
||||||
|
expect(screen.getByRole('tab', { name: '02 Bronnen' }).getAttribute('aria-selected')).toBe('true')
|
||||||
|
expect(screen.getByRole('tabpanel').textContent).toContain('Hou herkomst en dekking zichtbaar')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { BadgeCheck, Database, Download, MapPinned, ScanSearch } from 'lucide-react'
|
||||||
|
import '../../styles/landing-project-story.css'
|
||||||
|
|
||||||
|
const stages = [
|
||||||
|
{
|
||||||
|
key: 'select',
|
||||||
|
number: '01',
|
||||||
|
label: 'Selecteer',
|
||||||
|
title: 'Teken één ruimtelijke vraag',
|
||||||
|
description: 'Een officiële grens of vrije AOI wordt de vaste context voor elke volgende stap.',
|
||||||
|
metric: '2,14 km²',
|
||||||
|
metricLabel: 'geselecteerd',
|
||||||
|
icon: MapPinned,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'source',
|
||||||
|
number: '02',
|
||||||
|
label: 'Bronnen',
|
||||||
|
title: 'Hou herkomst en dekking zichtbaar',
|
||||||
|
description: 'Bronautoriteit, CRS, meetmoment en lokale dekking blijven aan ieder resultaat gekoppeld.',
|
||||||
|
metric: '7',
|
||||||
|
metricLabel: 'bronnen gecontroleerd',
|
||||||
|
icon: Database,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'analyse',
|
||||||
|
number: '03',
|
||||||
|
label: 'Analyseer',
|
||||||
|
title: 'Laat GIS en PyTorch samenwerken',
|
||||||
|
description: 'Ruimtelijke selectie, beeldanalyse en regionale context komen samen zonder ontbrekende AI-configuratie te verbergen.',
|
||||||
|
metric: '186',
|
||||||
|
metricLabel: 'objecten gevonden',
|
||||||
|
icon: ScanSearch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'prove',
|
||||||
|
number: '04',
|
||||||
|
label: 'Onderbouw',
|
||||||
|
title: 'Controleer bewijs vóór export',
|
||||||
|
description: 'QA/QC, uitzonderingen en provenance maken het resultaat navolgbaar en verdedigbaar.',
|
||||||
|
metric: '0,91',
|
||||||
|
metricLabel: 'kwaliteitsscore',
|
||||||
|
icon: BadgeCheck,
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export function LandingProjectStory(): JSX.Element {
|
||||||
|
const [activeIndex, setActiveIndex] = useState(0)
|
||||||
|
const activeStage = stages[activeIndex]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="landing-project-story" aria-labelledby="project-story-title">
|
||||||
|
<div className="landing-project-story-copy">
|
||||||
|
<p className="landing-workflow-eyebrow">Interactieve projectillustratie</p>
|
||||||
|
<h2 id="project-story-title">Van gebied naar resultaat, zonder de bewijsvoering kwijt te raken</h2>
|
||||||
|
<p className="landing-project-story-intro">
|
||||||
|
Verken de vier schakels van de GeoIntel-keten. Elke stap verandert de kaart en toont welke context behouden blijft.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="landing-story-tabs" role="tablist" aria-label="GeoIntel-projectketen">
|
||||||
|
{stages.map(({ key, number, label, icon: Icon }, index) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
id={`story-tab-${key}`}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeIndex === index}
|
||||||
|
aria-controls="story-stage-panel"
|
||||||
|
tabIndex={activeIndex === index ? 0 : -1}
|
||||||
|
onClick={() => setActiveIndex(index)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
|
||||||
|
event.preventDefault()
|
||||||
|
const direction = event.key === 'ArrowRight' ? 1 : -1
|
||||||
|
setActiveIndex((current) => (current + direction + stages.length) % stages.length)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{number}</span>
|
||||||
|
<Icon aria-hidden="true" />
|
||||||
|
<strong>{label}</strong>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="landing-story-detail"
|
||||||
|
id="story-stage-panel"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-labelledby={`story-tab-${activeStage.key}`}
|
||||||
|
key={activeStage.key}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<small>Actieve schakel</small>
|
||||||
|
<h3>{activeStage.title}</h3>
|
||||||
|
<p>{activeStage.description}</p>
|
||||||
|
</div>
|
||||||
|
<span className="landing-story-metric">
|
||||||
|
<strong>{activeStage.metric}</strong>
|
||||||
|
<small>{activeStage.metricLabel}</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`landing-story-visual is-stage-${activeIndex + 1}`} aria-hidden="true">
|
||||||
|
<div className="landing-story-map" />
|
||||||
|
<svg viewBox="0 0 720 610" role="presentation">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="story-area" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stopColor="#8ee7d2" stopOpacity=".34" />
|
||||||
|
<stop offset="1" stopColor="#23a68e" stopOpacity=".1" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="story-glow" x="-80%" y="-80%" width="260%" height="260%">
|
||||||
|
<feGaussianBlur stdDeviation="9" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g className="story-tile-grid">
|
||||||
|
<path d="M0 122H720M0 244H720M0 366H720M0 488H720" />
|
||||||
|
<path d="M144 0V610M288 0V610M432 0V610M576 0V610" />
|
||||||
|
</g>
|
||||||
|
<path className="story-aoi" d="M210 152 493 123 582 284 515 473 258 504 139 319Z" fill="url(#story-area)" />
|
||||||
|
<path className="story-route" d="M157 419C231 345 236 223 339 241s121 126 219 45" />
|
||||||
|
<g className="story-buildings">
|
||||||
|
<path d="m260 286 48-15 21 68-49 15Z" />
|
||||||
|
<path d="m340 207 69-11 10 57-67 13Z" />
|
||||||
|
<path d="m392 330 74-23 28 88-76 24Z" />
|
||||||
|
<path d="m220 376 52-18 19 55-51 19Z" />
|
||||||
|
</g>
|
||||||
|
<g className="story-evidence">
|
||||||
|
<circle cx="307" cy="314" r="8" />
|
||||||
|
<circle cx="402" cy="224" r="8" />
|
||||||
|
<circle cx="445" cy="365" r="8" />
|
||||||
|
<circle cx="255" cy="396" r="8" />
|
||||||
|
</g>
|
||||||
|
<g className="story-pulse">
|
||||||
|
<circle cx="445" cy="365" r="27" filter="url(#story-glow)" />
|
||||||
|
<circle cx="445" cy="365" r="12" />
|
||||||
|
</g>
|
||||||
|
<path className="story-scan" d="M142 219H581" />
|
||||||
|
</svg>
|
||||||
|
<div className="landing-story-status">
|
||||||
|
<span><i /> Live analyse</span>
|
||||||
|
<strong>{activeStage.label}</strong>
|
||||||
|
<small>{activeStage.metricLabel}</small>
|
||||||
|
</div>
|
||||||
|
<div className="landing-story-export"><Download /><span>Controleerbaar resultaat</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
.landing-project-story {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, .88fr) minmax(30rem, 1.12fr);
|
||||||
|
gap: clamp(2.5rem, 6vw, 6rem);
|
||||||
|
align-items: center;
|
||||||
|
width: min(92rem, 100%);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding: clamp(5rem, 9vw, 8rem) clamp(1.25rem, 4vw, 4.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-project-story-copy > h2 {
|
||||||
|
max-width: 18ch;
|
||||||
|
margin: .75rem 0 0;
|
||||||
|
font-family: "Manrope", sans-serif;
|
||||||
|
font-size: clamp(2.25rem, 4vw, 4rem);
|
||||||
|
letter-spacing: -.055em;
|
||||||
|
line-height: 1.04;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-project-story-intro {
|
||||||
|
max-width: 60ch;
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
|
color: var(--landing-muted);
|
||||||
|
font-size: .9rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-story-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: .45rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-story-tabs button {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
gap: .45rem;
|
||||||
|
justify-items: start;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--landing-line);
|
||||||
|
border-radius: .8rem;
|
||||||
|
padding: .8rem;
|
||||||
|
background: rgba(255, 255, 255, .72);
|
||||||
|
color: var(--landing-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform .24s ease, border-color .24s ease, background .24s ease, color .24s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-story-tabs button::after {
|
||||||
|
position: absolute;
|
||||||
|
right: .8rem;
|
||||||
|
bottom: 0;
|
||||||
|
left: .8rem;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--landing-primary);
|
||||||
|
content: '';
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleX(.2);
|
||||||
|
transition: opacity .24s ease, transform .24s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-story-tabs button:hover { transform: translateY(-2px); }
|
||||||
|
.landing-story-tabs button[aria-selected='true'] {
|
||||||
|
border-color: #83c8bb;
|
||||||
|
background: var(--landing-mint);
|
||||||
|
color: var(--landing-primary-strong);
|
||||||
|
}
|
||||||
|
.landing-story-tabs button[aria-selected='true']::after { opacity: 1; transform: scaleX(1); }
|
||||||
|
.landing-story-tabs button > span { font-size: .54rem; font-weight: 800; letter-spacing: .1em; }
|
||||||
|
.landing-story-tabs svg { width: 1rem; height: 1rem; }
|
||||||
|
.landing-story-tabs strong { overflow: hidden; max-width: 100%; font-size: .65rem; text-overflow: ellipsis; }
|
||||||
|
|
||||||
|
.landing-story-detail {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: end;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-left: 3px solid var(--landing-primary);
|
||||||
|
border-radius: 0 .8rem .8rem 0;
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
background: linear-gradient(90deg, rgba(222, 247, 241, .8), rgba(247, 252, 251, .55));
|
||||||
|
animation: landing-story-enter .42s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-story-detail small { color: var(--landing-primary); font-size: .55rem; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; }
|
||||||
|
.landing-story-detail h3 { margin: .25rem 0 0; font-family: "Manrope", sans-serif; font-size: 1rem; }
|
||||||
|
.landing-story-detail p { max-width: 54ch; margin: .35rem 0 0; color: var(--landing-muted); font-size: .69rem; line-height: 1.6; }
|
||||||
|
.landing-story-metric { display: grid; min-width: 6.5rem; text-align: right; }
|
||||||
|
.landing-story-metric strong { color: var(--landing-primary-strong); font-family: "Manrope", sans-serif; font-size: 1.75rem; line-height: 1; }
|
||||||
|
.landing-story-metric small { margin-top: .3rem; letter-spacing: .04em; }
|
||||||
|
|
||||||
|
.landing-story-visual {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
min-height: 38rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(161, 215, 204, .6);
|
||||||
|
border-radius: 1.6rem;
|
||||||
|
background: #072f2c;
|
||||||
|
box-shadow: 0 35px 90px rgba(10, 67, 59, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-story-map {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(145deg, rgba(4, 35, 32, .2), rgba(2, 26, 24, .78)), url('/portfolio/geointel-building-qa.png') center / cover;
|
||||||
|
filter: saturate(.78) contrast(1.03);
|
||||||
|
transform: scale(1.035);
|
||||||
|
transition: transform 1.1s cubic-bezier(.22, 1, .36, 1), filter .5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-story-visual:hover .landing-story-map { filter: saturate(.95) contrast(1.03); transform: scale(1.065); }
|
||||||
|
.landing-story-visual svg { position: absolute; z-index: 1; inset: 0; width: 100%; height: 100%; }
|
||||||
|
.story-tile-grid { fill: none; stroke: rgba(156, 229, 214, .12); stroke-width: 1; }
|
||||||
|
.story-aoi { stroke: #8ee7d2; stroke-width: 3; stroke-dasharray: 10 7; transition: opacity .4s ease, transform .55s ease; transform-origin: center; }
|
||||||
|
.story-route { fill: none; stroke: #8ee7d2; stroke-width: 4; stroke-dasharray: 9 12; stroke-linecap: round; animation: landing-story-route 12s linear infinite; }
|
||||||
|
.story-buildings { fill: rgba(102, 222, 197, .12); stroke: #a3f0df; stroke-width: 3; transition: opacity .35s ease; }
|
||||||
|
.story-evidence { fill: #f5c267; stroke: #fff4d4; stroke-width: 2; transition: opacity .35s ease; }
|
||||||
|
.story-pulse { fill: rgba(110, 236, 207, .68); stroke: #c8fff1; stroke-width: 2; animation: landing-story-pulse 2.8s ease-out infinite; transform-origin: 445px 365px; }
|
||||||
|
.story-scan { fill: none; stroke: rgba(190, 255, 240, .78); stroke-width: 2; filter: drop-shadow(0 0 7px #7be6d1); animation: landing-story-scan 5.5s ease-in-out infinite; }
|
||||||
|
.is-stage-1 .story-buildings, .is-stage-1 .story-evidence { opacity: .22; }
|
||||||
|
.is-stage-2 .story-evidence { opacity: .28; }
|
||||||
|
.is-stage-3 .story-aoi { transform: scale(.94); }
|
||||||
|
.is-stage-4 .story-buildings { opacity: .5; }
|
||||||
|
|
||||||
|
.landing-story-status, .landing-story-export {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
display: grid;
|
||||||
|
border: 1px solid rgba(196, 244, 234, .24);
|
||||||
|
border-radius: .9rem;
|
||||||
|
background: rgba(3, 39, 35, .78);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 16px 40px rgba(0, 0, 0, .24);
|
||||||
|
backdrop-filter: blur(13px);
|
||||||
|
}
|
||||||
|
.landing-story-status { top: 1.25rem; left: 1.25rem; gap: .2rem; min-width: 10rem; padding: .8rem .95rem; }
|
||||||
|
.landing-story-status span { color: #8ee7d2; font-size: .52rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; }
|
||||||
|
.landing-story-status i { display: inline-block; width: .42rem; height: .42rem; margin-right: .4rem; border-radius: 50%; background: #77e2ca; box-shadow: 0 0 0 5px rgba(119, 226, 202, .11); }
|
||||||
|
.landing-story-status strong { font-family: "Manrope", sans-serif; font-size: .92rem; }
|
||||||
|
.landing-story-status small { color: rgba(255, 255, 255, .58); font-size: .59rem; }
|
||||||
|
.landing-story-export { right: 1.25rem; bottom: 1.25rem; grid-template-columns: auto 1fr; gap: .55rem; align-items: center; padding: .75rem .9rem; font-size: .62rem; }
|
||||||
|
.landing-story-export svg { position: static; width: 1rem; height: 1rem; color: #8ee7d2; }
|
||||||
|
|
||||||
|
@keyframes landing-story-enter { from { opacity: 0; transform: translateY(8px); } }
|
||||||
|
@keyframes landing-story-route { to { stroke-dashoffset: -84; } }
|
||||||
|
@keyframes landing-story-pulse { 50% { opacity: .45; transform: scale(1.45); } }
|
||||||
|
@keyframes landing-story-scan { 0%, 100% { opacity: .12; transform: translateY(-120px); } 50% { opacity: .8; transform: translateY(250px); } }
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.landing-project-story { grid-template-columns: 1fr; }
|
||||||
|
.landing-project-story-copy > h2 { max-width: 21ch; }
|
||||||
|
.landing-story-visual { min-height: 34rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.landing-project-story { padding: 4rem 1rem; }
|
||||||
|
.landing-project-story-copy > h2 { font-size: 2.15rem; }
|
||||||
|
.landing-story-tabs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.landing-story-detail { grid-template-columns: 1fr; }
|
||||||
|
.landing-story-metric { text-align: left; }
|
||||||
|
.landing-story-visual { min-height: 28rem; border-radius: 1.15rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.landing-project-story *, .landing-project-story *::before, .landing-project-story *::after {
|
||||||
|
animation: none !important;
|
||||||
|
transition-duration: .01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -234,7 +234,7 @@ body.landing-body {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
background:
|
background:
|
||||||
linear-gradient(112deg, rgba(244, 250, 248, 1) 8%, rgba(241, 249, 247, 0.96) 45%, rgba(225, 241, 237, 0.77) 100%),
|
linear-gradient(112deg, rgba(244, 250, 248, 1) 8%, rgba(241, 249, 247, 0.96) 45%, rgba(225, 241, 237, 0.77) 100%),
|
||||||
url('/landing-hero-belgium.webp') center / cover;
|
url('/portfolio/geointel-belgium-north-sea-hero.png') center / cover;
|
||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||