# Architecture

## Page Routing
- `Home.py` → thin wrapper, minimal logic
- `pages/1_🗳️_Stemwijzer.py` → thin wrapper delegating to quiz module
- `pages/2_🔍_Explorer.py` → thin wrapper delegating to `explorer.py`
- **Pattern**: thin Streamlit page files that import and call into core modules

## Core Modules
```
database.py          → MotionDatabase singleton (shared across all pages)
explorer.py          → Explorer page logic, tab routing
explorer_helpers.py  → Pure functions, chart builders, coordinate computation
analysis/            → SVD, UMAP, clustering algorithms
pipeline/            → Data ingestion pipeline
config.py            → Dataclass Config, PARTY_COLOURS dict
```

## Data Flow
```
DuckDB → MotionDatabase (singleton)
              ↓
        st.cache_data loaders
              ↓
        explorer_helpers (pure functions)
              ↓
        Plotly charts → Streamlit
```

## Key Patterns
1. **Singleton per module**: `database.py` exports one `db` instance; `config.py` exports config + PARTY_COLOURS
2. **Graceful degradation**: try/except around optional dependencies (UMAP, Plotly)
3. **Pipeline**: fetch → transform → store (see `pipeline/` directory)
4. **API client**: with retry/backoff for external data sources
5. **Dummy fallbacks**: if optional dep unavailable, use dummy stub

## Database Schema (key relationships)
```
motions (id, title, date, category)
  ↓
mp_votes (mp_id, motion_id, vote: -1/0/1)
  ↓
svd_vectors (entity_id, window, vector_2d)  ← entity_id = mp_name OR party_name
  ↓
party_centroids (party, window, centroid_2d)
  ↓
mp_party_history (mp_id, party, start_date, end_date)
```

## SVD Computation Pipeline
1. Build MP × Motion vote matrix from `mp_votes`
2. Run SVD to get 2D embeddings per MP
3. Optionally aggregate to party centroids
4. Align across windows using Procrustes
5. Store in `svd_vectors` table
