{{ application.job.original_title }}
{{ application.job.employer_name }} · {{ application.job.raw_location }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01db81c..3ddb0d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,25 @@
Alle betekenisvolle wijzigingen worden hier bijgehouden. Het project volgt voorlopig een pre-1.0 semantische versieaanpak.
+## 0.2.1 — 2026-07-21
+
+### Toegevoegd
+
+- Productieklare vertaling van de negen Stitch Intelligence Cockpit-schermen naar de bestaande Django-kern.
+- Gedeelde responsive shell, lokale SVG-iconsprite, donkere en lichte thematokens en toegankelijke mobiele navigatie.
+- Cockpitdashboard, vacature-intelligentiekaarten/detail, sollicitatiepipeline, gegroepeerde profieleditor, bronkaarten en systeemstatus.
+- Integratieplan, permanente Stitch-referentie en browservalidatiescreenshots voor zes doelviewports.
+
+### Gewijzigd
+
+- Alle kernroutes gebruiken nu één CSP-veilige lokale CSS/JS-laag zonder externe productie-assets.
+- Bronbeheer is teruggebracht onder de gedeelde shell terwijl import, bulkacties, retry, policy en SSRF-beveiliging intact blijven.
+- Browseropslag voor themavoorkeur is progressief: opslagbeperkingen veroorzaken geen console- of bedieningsfout.
+
+### Niet overgenomen
+
+- Quick/direct apply, fictieve workerstatistieken, onbewezen werkgeversinformatie en kunstmatige activiteitenfeeds.
+
## 0.1.1 — 2026-07-21
### Toegevoegd
diff --git a/VERIFICATION_REPORT.md b/VERIFICATION_REPORT.md
index 5a16640..723cacd 100644
--- a/VERIFICATION_REPORT.md
+++ b/VERIFICATION_REPORT.md
@@ -76,3 +76,19 @@ Codex krijgt vaste productbeslissingen via `AGENTS.md`, `CODEX_START_HERE.md`, a
Docker was in de assemblageomgeving niet beschikbaar. Daarom zijn de Dockerfile en Composebestanden statisch gevalideerd, maar een echte imagebuild, PostgreSQL/Redis-container-smoke, Unraid-installatie en restore-drill worden niet als uitgevoerd geclaimd. `VR-115` en `VR-117` bevatten de exacte acceptatiecriteria en commando's voor een Docker-capabele omgeving.
Live bronvoorwaarden, mailboxcredentials, SMTP, domein/TLS, registry en een gelicenseerde Belgische geodatabron vereisen persoonlijke/externe configuratie en staan apart als `blocked-external`.
+
+## VR-118 — Stitch Intelligence Cockpit (2026-07-21)
+
+Aanvullend op de bestaande releasebasis is de volledige interface opnieuw geïntegreerd en gecontroleerd.
+
+Uitgevoerde browsermatrix:
+
+- 1440×900, 1280×800, 1024×768, 768×1024, 390×844 en 360×800;
+- exact één `h1` en geen documentbrede horizontale overflow op ieder formaat;
+- dashboard, vacaturelijst/detail, sollicitatiepipeline, profiel/editor, bronnen en systeem;
+- dark en light, mobiele open/sluitnavigatie, bron-disclosure en vacature → gesolliciteerd → pipeline;
+- geen externe productie-scripts of stylesheets en geen nieuwe consolefouten na de storagefallbackfix.
+
+Screenshots: `docs/design/validation/`. Deze map en de originele Stitch-referentie zijn via `.dockerignore` uitgesloten van de productiecontainer.
+
+De volledige afrondingsgate en actuele aantallen worden onderaan dit rapport toegevoegd na `scripts/codex_verify.sh`.
diff --git a/apps/core/context_processors.py b/apps/core/context_processors.py
index 6fa8527..f0ff6ce 100644
--- a/apps/core/context_processors.py
+++ b/apps/core/context_processors.py
@@ -8,5 +8,5 @@ from django.http import HttpRequest
def navigation_context(request: HttpRequest) -> dict[str, Any]:
return {
"app_name": "VacatureRadar",
- "app_version": "0.1.0",
+ "app_version": "0.2.1",
}
diff --git a/apps/jobs/views.py b/apps/jobs/views.py
index 3ebe4a5..73611cd 100644
--- a/apps/jobs/views.py
+++ b/apps/jobs/views.py
@@ -108,6 +108,20 @@ class ApplicationListView(LoginRequiredMixin, ListView):
"job", "job__employer"
)
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ applications = list(context["applications"])
+ context["applications"] = applications
+ context["pipeline_columns"] = [
+ {
+ "status": status,
+ "label": label,
+ "applications": [item for item in applications if item.status == status],
+ }
+ for status, label in Application.Status.choices
+ ]
+ return context
+
class ApplicationUpdateView(LoginRequiredMixin, UpdateView):
model = Application
diff --git a/docs/ai/BACKLOG.yaml b/docs/ai/BACKLOG.yaml
index da09083..84a134c 100644
--- a/docs/ai/BACKLOG.yaml
+++ b/docs/ai/BACKLOG.yaml
@@ -941,6 +941,42 @@ tasks:
completed_at: '2026-07-21'
note: Release script, smoke-run, backup/restore and artifact bundle for VR-117 toegevoegd; release artefacten
en Unraid-runbook geactualiseerd; traceability en projectstate bijgewerkt.
+- id: VR-118
+ title: Stitch Intelligence Cockpit production-grade integreren
+ status: done
+ priority: P1
+ requirement_ids:
+ - NFR-006
+ - NFR-008
+ - AC-002
+ - AC-003
+ - AC-051
+ depends_on:
+ - VR-107
+ - VR-114
+ summary: Vertaal de negen aangeleverde Stitch-schermen naar de bestaande server-rendered Django-interface zonder
+ domeinlogica, securitygrenzen of gebruikersscope te vervangen en zonder fictieve productdata te introduceren.
+ acceptance_criteria:
+ - Een consistente responsive cockpit-shell en dark/light tokens bedienen alle kernroutes.
+ - Dashboard, vacatures, detail, pipeline, profiel, bronnen, systeem en login gebruiken echte backenddata en acties.
+ - Externe auto-apply, nepmetrics, onbewezen werkgeverinzichten en onbetrouwbare activiteitenfeeds zijn niet overgenomen.
+ - Zes doelviewports hebben één h1, geen horizontale overflow en werkende mobiele navigatie.
+ - Stitch-bronnen, integratieplan, screenshots en verificatierapport zijn traceerbaar maar niet onderdeel van de productiecontainer.
+ verification:
+ - uv run pytest tests/e2e tests/integration/test_views.py tests/integration/test_tasks_and_more_views.py
+ - ./scripts/codex_verify.sh
+ - Browsercontrole op 1440x900, 1280x800, 1024x768, 768x1024, 390x844 en 360x800
+ primary_paths:
+ - templates/
+ - static/css/app.css
+ - static/js/app.js
+ - docs/design/STITCH_INTEGRATION_PLAN.md
+ - docs/design/stitch-reference/
+ - docs/design/validation/
+ result:
+ completed_at: '2026-07-21'
+ note: Stitch-visuele taal geïntegreerd in CSP-veilige Django-templates; dark/light, responsive shell, pipeline,
+ broninteracties en browservalidatie toegevoegd zonder functionele scope-uitbreiding of fictieve data.
- id: VR-201
title: Concrete live-bronnen inventariseren en juridisch/operationeel reviewen
status: blocked-external
diff --git a/docs/ai/PROJECT_STATE.md b/docs/ai/PROJECT_STATE.md
index 731b98c..123247c 100644
--- a/docs/ai/PROJECT_STATE.md
+++ b/docs/ai/PROJECT_STATE.md
@@ -1,7 +1,7 @@
# Projectstatus
- Laatst bijgewerkt: 2026-07-21
-- Repositoryversie: 0.1.0 MVP-basis
+- Repositoryversie: 0.2.1 Stitch Intelligence Cockpit
- Uitvoeringsmodus: autonome backlog
- Eerste uitvoerbare taak: controleer met `python scripts/backlog.py next`
@@ -14,7 +14,7 @@ De repository bevat een uitvoerbare Django-MVP met:
- JSON-LD-, HTML-, RSS/Atom- en e-mailadapters;
- normalisatie, sanitization, werkgeverresolutie, dedupe, aliassen, provenance en versies;
- deterministische features, harde exclusions, scores en optionele uitgeschakelde Ollama-boundary;
-- dashboard, vacaturelijst/detail, profiel-, bron- en sollicitatiepagina's;
+- responsive dark/light cockpitdashboard, vacaturelijst/detail, sollicitatiepipeline, profiel-, bron- en systeemschermen;
- feedbackacties, applicationdossiers, lifecycle en dagelijkse digest-outbox;
- Celeryworker/scheduler, healthchecks, Docker/Unraidconfiguratie en back-up-/herstelscripts;
- offline unit-, integratie- en securitytests.
@@ -43,6 +43,13 @@ Release-audit op 2026-07-21:
## Laatste uitgevoerde backlogtaak
+- 2026-07-21: `VR-118` voltooid.
+ - Alle kernroutes delen nu één responsive Intelligence Cockpit-shell met lokale SVG-iconen en CSP-veilige assets.
+ - Stitch-dark en een volwaardige light variant zijn gekoppeld aan echte Django-data; externe quick apply, nepmetrics en onbewezen intelligence zijn bewust geweerd.
+ - Browserflow valideerde vacature → sollicitatiedossier → pipeline, bron-disclosures, themawissel en viewports 1440×900, 1280×800, 1024×768, 768×1024, 390×844 en 360×800 zonder horizontale overflow.
+ - Stitch-bronnen, integratieplan en screenshots staan onder `docs/design/` en zijn uit de productiecontainer gesloten.
+ - Volgende taak: `VR-201` (`blocked-external`).
+
- 2026-07-21: `VR-117` voltooid.
- Productiereleasecontrole toegevoegd met `scripts/release_verify.sh`, inclusief smoke, backup/restore-smoke en artifactbundel.
- Changelog, traceability en Unraid-runbook samengekoppeld aan het releasepad.
@@ -156,7 +163,7 @@ Deze blokkeren onafhankelijke code niet:
## Bekende verificatiebeperking van de aangeleverde basis
-De Pythonapplicatie, Dockerimage en PostgreSQL/Redis/Unraid-stack zijn geverifieerd. De Playwright-browservarianten zijn lokaal niet uitgevoerd; hun HTML/a11y-fallback is wel geslaagd. Een publieke HTTPS-smoke blijft afhankelijk van domein-, DNS- en reverse-proxyconfiguratie.
+De Pythonapplicatie, Dockerimage en PostgreSQL/Redis/Unraid-stack zijn geverifieerd. De optionele pytest-Playwrightvarianten vallen lokaal terug op de HTML/a11y-probe; VR-118 is aanvullend handmatig met de geïntegreerde browser op zes viewports uitgevoerd. Een publieke HTTPS-smoke blijft afhankelijk van domein-, DNS- en reverse-proxyconfiguratie.
## Hervatten
diff --git a/docs/design/ACCESSIBILITY.md b/docs/design/ACCESSIBILITY.md
index 89c191c..863975c 100644
--- a/docs/design/ACCESSIBILITY.md
+++ b/docs/design/ACCESSIBILITY.md
@@ -62,3 +62,12 @@ Per zichtbare taak:
4. screenreader smoke test voor dashboard, vacaturedetail en profiel;
5. contrastcontrole;
6. test met lange/lege/unknown content.
+
+## VR-118 browservalidatie
+
+- Dashboard gecontroleerd op 1440×900, 1280×800, 1024×768, 768×1024, 390×844 en 360×800.
+- Op alle zes viewports: exact één `h1`, geen documentbrede horizontale overflow en de juiste desktop/mobile navigatiestatus.
+- Mobiel menu opent met `aria-expanded=true`, sluit met Escape/sluitactie en is buiten de focus-/accessibilityflow wanneer gesloten.
+- Themawisselaar blijft bruikbaar wanneer browseropslag niet beschikbaar is; de UI geeft dan geen consolefout.
+- Dashboard, vacaturelijst/detail, sollicitatiepipeline, profielen/editor, bronnen en systeem zijn met echte seeded data en zonder externe assets gevalideerd.
+- Screenshots staan onder `docs/design/validation/` en zijn via `.dockerignore` uitgesloten van de productiecontainer.
diff --git a/docs/design/DESIGN_SYSTEM.md b/docs/design/DESIGN_SYSTEM.md
index f02058d..bb14ff7 100644
--- a/docs/design/DESIGN_SYSTEM.md
+++ b/docs/design/DESIGN_SYSTEM.md
@@ -1,37 +1,44 @@
# Design system
+> Geactualiseerd voor VR-118. De volledige Stitch-mapping en bewuste afwijkingen staan in
+> `docs/design/STITCH_INTEGRATION_PLAN.md`.
+
## 1. Merkgevoel
-Rustig, betrouwbaar, helder en licht technisch. De interface moet focus geven, niet gamificeren. Geen confetti, eindeloze badges of agressieve notificatiekleuren.
+Rustig, betrouwbaar, helder en technisch precies. De interface voelt als een professionele intelligence-cockpit:
+donker marineblauw als standaard, signaalcyaan voor primaire interactie en tonale lagen in plaats van zware schaduwen.
+De lichte variant behoudt dezelfde hiërarchie. Geen confetti, fictieve grafieken, eindeloze badges of agressieve notificatiekleuren.
## 2. Visuele tokens
De implementatie staat in `static/css/app.css`. Gebruik semantische custom properties in plaats van losse kleuren in componenten.
-Aanbevolen tokenrollen:
+Geïmplementeerde tokenrollen omvatten onder meer:
```css
---surface-page
---surface-card
---surface-raised
---text-primary
---text-secondary
---text-muted
---border-subtle
---accent
---accent-strong
---success
+--bg
+--bg-deep
+--surface
+--surface-low
+--surface-high
+--surface-bright
+--text
+--text-soft
+--muted
+--border
+--border-strong
+--primary
+--primary-strong
+--positive
--warning
--danger
---focus-ring
---shadow-card
+--signal-glow
--radius-sm
---radius-md
---radius-lg
---space-1 ... --space-8
+--radius
```
-Donkere modus volgt `prefers-color-scheme` en behoudt semantische contrastrollen.
+Donkere modus is de productstandaard. De gebruiker kan lokaal wisselen via `data-theme`; opslag is progressief en
+mag in een afgeschermde browsercontext uitvallen zonder de bediening te breken.
## 3. Typografie
@@ -45,10 +52,11 @@ Donkere modus volgt `prefers-color-scheme` en behoudt semantische contrastrollen
## 4. Spacing en layout
- 4/8px-gebaseerde schaal;
-- contentcontainer ongeveer 1180–1280px;
+- contentcontainer maximaal 1600px zodat de cockpit op brede schermen bruikbaar blijft;
- kaarten met consistente padding 20–24px desktop, 16px mobiel;
- minimaal 44×44px touch targets voor primaire interactie;
-- grids breken onder 900px en 640px logisch af;
+- vaste zijbalk van 272px boven 900px, toegankelijke uitschuifnavigatie daaronder;
+- grids breken onder 1180px, 900px en 680px logisch af;
- vaste/sticky elementen mogen content niet bedekken.
## 5. Componenten
diff --git a/docs/design/validation/applications-dark-1280x800.png b/docs/design/validation/applications-dark-1280x800.png
new file mode 100644
index 0000000..a0c802a
Binary files /dev/null and b/docs/design/validation/applications-dark-1280x800.png differ
diff --git a/docs/design/validation/dashboard-1024x768.png b/docs/design/validation/dashboard-1024x768.png
new file mode 100644
index 0000000..ede3317
Binary files /dev/null and b/docs/design/validation/dashboard-1024x768.png differ
diff --git a/docs/design/validation/dashboard-1280x800.png b/docs/design/validation/dashboard-1280x800.png
new file mode 100644
index 0000000..878abc4
Binary files /dev/null and b/docs/design/validation/dashboard-1280x800.png differ
diff --git a/docs/design/validation/dashboard-1440x900.png b/docs/design/validation/dashboard-1440x900.png
new file mode 100644
index 0000000..ee8f47a
Binary files /dev/null and b/docs/design/validation/dashboard-1440x900.png differ
diff --git a/docs/design/validation/dashboard-360x800.png b/docs/design/validation/dashboard-360x800.png
new file mode 100644
index 0000000..cc3a42d
Binary files /dev/null and b/docs/design/validation/dashboard-360x800.png differ
diff --git a/docs/design/validation/dashboard-390x844.png b/docs/design/validation/dashboard-390x844.png
new file mode 100644
index 0000000..a66adaf
Binary files /dev/null and b/docs/design/validation/dashboard-390x844.png differ
diff --git a/docs/design/validation/dashboard-768x1024.png b/docs/design/validation/dashboard-768x1024.png
new file mode 100644
index 0000000..4c2639b
Binary files /dev/null and b/docs/design/validation/dashboard-768x1024.png differ
diff --git a/docs/design/validation/dashboard-light-1280x800.png b/docs/design/validation/dashboard-light-1280x800.png
new file mode 100644
index 0000000..50c8723
Binary files /dev/null and b/docs/design/validation/dashboard-light-1280x800.png differ
diff --git a/docs/design/validation/dashboard-light-1440x900.png b/docs/design/validation/dashboard-light-1440x900.png
new file mode 100644
index 0000000..a0449a8
Binary files /dev/null and b/docs/design/validation/dashboard-light-1440x900.png differ
diff --git a/docs/design/validation/job-detail-dark-1280x800.png b/docs/design/validation/job-detail-dark-1280x800.png
new file mode 100644
index 0000000..1389179
Binary files /dev/null and b/docs/design/validation/job-detail-dark-1280x800.png differ
diff --git a/docs/design/validation/jobs-dark-1280x800.png b/docs/design/validation/jobs-dark-1280x800.png
new file mode 100644
index 0000000..7e0dc75
Binary files /dev/null and b/docs/design/validation/jobs-dark-1280x800.png differ
diff --git a/docs/design/validation/mobile-navigation-390x844.png b/docs/design/validation/mobile-navigation-390x844.png
new file mode 100644
index 0000000..ff5fe2d
Binary files /dev/null and b/docs/design/validation/mobile-navigation-390x844.png differ
diff --git a/docs/design/validation/profile-editor-dark-1280x800.png b/docs/design/validation/profile-editor-dark-1280x800.png
new file mode 100644
index 0000000..2bffde8
Binary files /dev/null and b/docs/design/validation/profile-editor-dark-1280x800.png differ
diff --git a/docs/design/validation/profiles-dark-1280x800.png b/docs/design/validation/profiles-dark-1280x800.png
new file mode 100644
index 0000000..340ddb2
Binary files /dev/null and b/docs/design/validation/profiles-dark-1280x800.png differ
diff --git a/docs/design/validation/sources-dark-1280x800.png b/docs/design/validation/sources-dark-1280x800.png
new file mode 100644
index 0000000..2fff512
Binary files /dev/null and b/docs/design/validation/sources-dark-1280x800.png differ
diff --git a/docs/design/validation/system-dark-1280x800.png b/docs/design/validation/system-dark-1280x800.png
new file mode 100644
index 0000000..03659b6
Binary files /dev/null and b/docs/design/validation/system-dark-1280x800.png differ
diff --git a/docs/quality/THREAT_MODEL.md b/docs/quality/THREAT_MODEL.md
index ed997d8..0fa3337 100644
--- a/docs/quality/THREAT_MODEL.md
+++ b/docs/quality/THREAT_MODEL.md
@@ -72,6 +72,7 @@ Django/Celery
| T-24 | Source terms wijzigen | ongewenste voortgezette crawling | reviewdatum, source health en policy expiry gepland | `VR-102`, `VR-112` |
| T-31 | Sollicitatiedossier-export bevat niet-gewenste payload | data-lek of onbedoeld dossieroverdragen | export bevat alleen het gekozen dossier, ZIP-inhoud is beperkt tot snapshot/tijdlijn/print-HTML, bestandsnaam is geslugified zonder padseparators | `tests/integration/test_applications.py` |
| T-32 | XML external entity/entity-expansion via sitemap | lokale data-uitlezing of parseruitputting | sitemap-XML wordt met `defusedxml` verwerkt; fetcherlimieten blijven vóór parsing gelden | `tests/unit/test_discovery_rss.py`, Ruff securitycheck |
+| T-33 | Externe UI-CDN of niet-vertrouwde Stitch-interactie | supply-chain-XSS, tracking of acties buiten Django-beleid | productie gebruikt alleen lokale CSS/JS en inline SVG-symbolen; Stitch-code is documentatiereferentie, alle mutaties blijven Django POST+CSRF; themavoorkeur bevat geen persoonsgegevens en opslagfouten worden afgevangen | `tests/e2e/test_vr114_browser_and_accessibility.py`, browserconsole- en assetcontrole VR-118 |
## Misbruikscenario's
diff --git a/docs/quality/TRACEABILITY_MATRIX.md b/docs/quality/TRACEABILITY_MATRIX.md
index cacc977..86a67f7 100644
--- a/docs/quality/TRACEABILITY_MATRIX.md
+++ b/docs/quality/TRACEABILITY_MATRIX.md
@@ -39,8 +39,8 @@ Legenda: **I** = geïmplementeerd en getest in de huidige MVP; **P** = gedeeltel
| PR-044 | I | profiel/modelversievelden | scoring/profiletests | VR-109 promptversie |
| PR-045 | I | optionele Ollamaservice/fallback | `test_ai.py` | VR-109 |
| PR-046 | I | geen tools + guardrails | AI/securitytests | VR-109/115 evaluaties |
-| PR-050 | I | dashboard view/template | viewtests | VR-114 e2e/a11y |
-| PR-051 | I | detailview/template/provenance | viewtests | VR-114 |
+| PR-050 | I | dashboard view/template + Stitch-cockpitshell | viewtests, VR-118 browsermatrix | VR-114/118 e2e/a11y |
+| PR-051 | I | detailview/template/provenance + intelligence-layout | viewtests, VR-118 screenshots | VR-114/118 |
| PR-052 | I | feedbackendpoint/UI | feedback/viewtests | — |
| PR-053 | I | applicationmodel/serviceflow | `tests/integration/test_dedupe_lifecycle_feedback.py`, `tests/integration/test_applications.py` | VR-113 uitgebreid dossier |
| PR-054 | I | `notifications/models.py`, outbox/services/tasks | `tests/integration/test_email_import_notifications.py`, `tests/integration/test_reminders.py` | VR-111 |
@@ -63,7 +63,7 @@ Legenda: **I** = geïmplementeerd en getest in de huidige MVP; **P** = gedeeltel
| NFR-005 | I | domeinapps/services/adapters/ADRs | continue review |
| NFR-006 | I | offline pytest + coveragegate | VR-114 verbreedt e2e |
| NFR-007 | P | `scripts/benchmark.py`, `tests/benchmark/test_benchmark.py` | VR-116 |
-| NFR-008 | I | semantische templates/designspec | `tests/e2e/test_vr114_browser_and_accessibility.py` |
+| NFR-008 | I | semantische templates/designspec, zes viewports, dark/light en toetsenbordmenu | `tests/e2e/test_vr114_browser_and_accessibility.py`, `docs/design/validation/`, VR-118 |
| NFR-009 | I | lokale SQLite en Docker/Unraidconfig, release-smoke en restore | VR-117 afgerond |
| NFR-010 | I | `apps/sources/services/health.py`, `apps/sources/tasks.py`, `apps/core/views.py`, `config/settings.py` | `tests/integration/test_source_health.py` | VR-112 observability |
@@ -87,5 +87,6 @@ Iedere backlogtaak die een requirement wijzigt, werkt in dezelfde commit deze ma
- VR-102 is gerealiseerd met auditbare policyreviews (`SourcePolicyReview`), robotscache met TTL/size/SSRF-controles en fail-closed policy-gating op verlopen of conflicterende reviews.
- VR-107 is gerealiseerd met `apps/sources/views.py`, `apps/sources/services/manual_import.py`, `templates/sources/list.html`, `static/js/manual_import.js` en bijhorende unit/integration-tests.
- VR-115 is afgerond met productie-hardening voor `config/settings.py`, DNS-rebindmitigatie in `apps/sources/services/fetcher.py`, rate limiting op login en handmatige import, CI-secret- en kwetsbaarheidsscans, non-root/read-only containerhardening en bijbehorende security/unit/integration-tests.
+- VR-118 vertaalt de aangeleverde Stitch Intelligence Cockpit naar de bestaande server-rendered routes. Bewijs staat in `docs/design/STITCH_INTEGRATION_PLAN.md`, `docs/design/validation/`, de view/a11ytests en de gedeelde lokale shell/assets.
diff --git a/static/css/app.css b/static/css/app.css
index ea9d1a9..fd407c0 100644
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -1,303 +1,382 @@
-:root {
- --bg: #f5f7fb;
- --surface: #ffffff;
- --surface-2: #eef2f8;
- --text: #142033;
- --muted: #667085;
- --border: #dce3ee;
- --primary: #465bd8;
- --primary-strong: #3446b9;
- --primary-soft: #edf0ff;
- --positive: #087f5b;
- --positive-soft: #e7f7f1;
- --warning: #9a6700;
- --warning-soft: #fff5d6;
- --danger: #b42318;
- --danger-soft: #ffebe9;
- --shadow: 0 16px 40px rgba(23, 36, 64, .08);
- --radius: 16px;
- --radius-sm: 10px;
- --sidebar: 248px;
+:root,
+:root[data-theme="dark"] {
+ color-scheme: dark;
+ --bg: #081120;
+ --bg-deep: #050c18;
+ --surface: #111b2e;
+ --surface-low: #0c1627;
+ --surface-high: #19253a;
+ --surface-bright: #243149;
+ --text: #e7efff;
+ --text-soft: #c7d3e7;
+ --muted: #8fa1b8;
+ --border: #2b3a50;
+ --border-strong: #42536b;
+ --primary: #16e5ee;
+ --primary-strong: #7cf8ff;
+ --primary-ink: #002f32;
+ --primary-soft: rgba(22, 229, 238, .1);
+ --positive: #4edea3;
+ --positive-soft: rgba(78, 222, 163, .11);
+ --warning: #ffca69;
+ --warning-soft: rgba(255, 202, 105, .11);
+ --danger: #ff8f87;
+ --danger-soft: rgba(255, 143, 135, .11);
+ --indigo: #9ea5ff;
+ --shadow: 0 18px 55px rgba(0, 0, 0, .24);
+ --signal-glow: 0 0 0 1px rgba(22, 229, 238, .12), 0 0 28px rgba(22, 229, 238, .07);
+ --radius: 9px;
+ --radius-sm: 5px;
+ --sidebar: 272px;
+ --utility: 66px;
}
-@media (prefers-color-scheme: dark) {
- :root {
- --bg: #0f1520;
- --surface: #171f2d;
- --surface-2: #202a3a;
- --text: #edf2fa;
- --muted: #a8b2c4;
- --border: #303b4e;
- --primary: #91a0ff;
- --primary-strong: #aab5ff;
- --primary-soft: #252f55;
- --positive: #5bd3aa;
- --positive-soft: #153b31;
- --warning: #ffd166;
- --warning-soft: #443716;
- --danger: #ff8c84;
- --danger-soft: #4b2424;
- --shadow: 0 18px 42px rgba(0, 0, 0, .28);
- }
+:root[data-theme="light"] {
+ color-scheme: light;
+ --bg: #f3f6fa;
+ --bg-deep: #e9eef5;
+ --surface: #ffffff;
+ --surface-low: #f7f9fc;
+ --surface-high: #edf2f7;
+ --surface-bright: #e2e9f2;
+ --text: #142033;
+ --text-soft: #2c3c52;
+ --muted: #617187;
+ --border: #d7e0eb;
+ --border-strong: #aebdce;
+ --primary: #007f87;
+ --primary-strong: #005e64;
+ --primary-ink: #f4ffff;
+ --primary-soft: rgba(0, 127, 135, .09);
+ --positive: #087f5b;
+ --positive-soft: rgba(8, 127, 91, .1);
+ --warning: #8b5b00;
+ --warning-soft: rgba(183, 119, 0, .11);
+ --danger: #b42318;
+ --danger-soft: rgba(180, 35, 24, .09);
+ --indigo: #4e56c8;
+ --shadow: 0 16px 44px rgba(27, 43, 65, .08);
+ --signal-glow: 0 0 0 1px rgba(0, 127, 135, .12), 0 12px 34px rgba(0, 70, 75, .08);
}
* { box-sizing: border-box; }
html { min-height: 100%; scroll-behavior: smooth; }
body {
+ min-width: 320px;
min-height: 100vh;
margin: 0;
- background: var(--bg);
+ overflow-x: hidden;
+ background:
+ linear-gradient(rgba(110, 150, 190, .025) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(110, 150, 190, .025) 1px, transparent 1px),
+ var(--bg);
+ background-size: 48px 48px;
color: var(--text);
font: 15px/1.55 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ -webkit-font-smoothing: antialiased;
}
-a { color: var(--primary); text-decoration: none; }
+a { color: var(--primary-strong); text-decoration: none; }
a:hover { text-decoration: underline; }
button, input, select, textarea { font: inherit; }
button { cursor: pointer; }
-code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
-pre { overflow: auto; border-radius: var(--radius-sm); background: var(--surface-2); padding: 18px; }
-
-.skip-link { position: fixed; left: 10px; top: -80px; z-index: 1000; padding: 10px 14px; background: var(--surface); color: var(--text); border-radius: 8px; box-shadow: var(--shadow); }
-.skip-link:focus { top: 10px; }
-.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
-.mobile-only { display: none; }
-
-.topbar {
- position: sticky;
- top: 0;
- z-index: 30;
- min-height: 70px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 20px;
- padding: 10px 24px;
- border-bottom: 1px solid var(--border);
- background: color-mix(in srgb, var(--surface) 92%, transparent);
- backdrop-filter: blur(16px);
+code, pre, .eyebrow, .pill, .meta-label, .nav-group-label, .job-meta, .utility-title, .kpi-label {
+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
}
-.brand-wrap, .brand, .topbar-actions { display: flex; align-items: center; gap: 12px; }
-.brand { color: var(--text); }
-.brand:hover { text-decoration: none; }
-.brand span:last-child { display: grid; line-height: 1.2; }
-.brand small { color: var(--muted); font-size: 12px; }
-.brand-mark {
- width: 42px;
- height: 42px;
- display: inline-grid;
- place-items: center;
- border-radius: 12px;
- background: linear-gradient(145deg, var(--primary), #7b4fe8);
- color: white;
- font-weight: 800;
- letter-spacing: -.04em;
- box-shadow: 0 8px 18px rgba(70, 91, 216, .25);
-}
-.app-shell { display: grid; grid-template-columns: var(--sidebar) minmax(0, 1fr); min-height: calc(100vh - 70px); }
+pre { overflow: auto; margin: 0; border-radius: var(--radius-sm); background: var(--bg-deep); padding: 18px; border: 1px solid var(--border); }
+svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
+.icon { width: 20px; height: 20px; flex: 0 0 auto; }
+.icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
+.skip-link { position: fixed; left: 12px; top: -90px; z-index: 1000; padding: 11px 15px; background: var(--primary); color: var(--primary-ink); border-radius: var(--radius-sm); font-weight: 800; }
+.skip-link:focus { top: 12px; }
+.sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important; }
+.mobile-only { display: none !important; }
+
+.app-frame { min-height: 100vh; }
.sidebar {
- position: sticky;
- top: 70px;
- height: calc(100vh - 70px);
+ position: fixed;
+ inset: 0 auto 0 0;
+ z-index: 60;
+ width: var(--sidebar);
display: flex;
flex-direction: column;
- gap: 6px;
- padding: 24px 14px;
- background: var(--surface);
+ padding: 18px 14px;
+ background: color-mix(in srgb, var(--bg-deep) 96%, transparent);
border-right: 1px solid var(--border);
- overflow-y: auto;
+ box-shadow: 12px 0 42px rgba(0, 0, 0, .08);
}
-.nav-link { display: grid; gap: 2px; padding: 11px 14px; border-radius: 11px; color: var(--text); font-weight: 650; }
-.nav-link small { color: var(--muted); font-weight: 400; }
-.nav-link:hover { background: var(--surface-2); text-decoration: none; }
-.nav-link.active { background: var(--primary-soft); color: var(--primary-strong); }
-.nav-link.active small { color: var(--primary); }
-.sidebar-foot { margin-top: auto; display: flex; align-items: center; gap: 7px; padding: 12px; color: var(--muted); font-size: 12px; }
-.sidebar-foot small { margin-left: auto; }
-.status-dot { width: 8px; height: 8px; border-radius: 99px; background: var(--positive); box-shadow: 0 0 0 4px var(--positive-soft); }
-.main-content { width: 100%; max-width: 1480px; padding: 34px clamp(20px, 4vw, 56px) 64px; }
-.auth-main { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: radial-gradient(circle at 20% 10%, var(--primary-soft), transparent 36%), var(--bg); }
+.sidebar-brand { display: flex; align-items: center; justify-content: space-between; padding: 2px 6px 22px; }
+.brand { min-width: 0; display: flex; align-items: center; gap: 12px; color: var(--text); }
+.brand:hover { text-decoration: none; }
+.brand > span:last-child { min-width: 0; display: grid; line-height: 1.2; }
+.brand strong { font-size: 18px; letter-spacing: -.025em; }
+.brand small { margin-top: 3px; color: var(--muted); font: 10px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; letter-spacing: .1em; text-transform: uppercase; }
+.brand-mark { width: 42px; height: 42px; display: inline-grid; place-items: center; border-radius: 7px; background: var(--primary); color: var(--primary-ink); box-shadow: 0 0 24px color-mix(in srgb, var(--primary) 25%, transparent); }
+.brand-mark .icon { width: 25px; height: 25px; stroke-width: 1.6; }
+.nav-stack { min-height: 0; overflow-y: auto; display: grid; align-content: start; gap: 4px; padding: 4px 0; }
+.nav-group-label { margin: 18px 13px 5px; color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: .13em; }
+.nav-group-label:first-child { margin-top: 3px; }
+.nav-link { position: relative; display: grid; grid-template-columns: 24px minmax(0, 1fr); align-items: center; gap: 12px; min-height: 54px; padding: 8px 12px; border: 1px solid transparent; border-radius: 7px; color: var(--muted); }
+.nav-link > span { min-width: 0; display: grid; color: var(--text-soft); font-weight: 700; line-height: 1.25; }
+.nav-link small { color: var(--muted); font-size: 11px; font-weight: 400; }
+.nav-link:hover { background: var(--surface); border-color: var(--border); text-decoration: none; color: var(--primary); }
+.nav-link.active { background: linear-gradient(90deg, var(--primary-soft), transparent); border-color: color-mix(in srgb, var(--primary) 25%, var(--border)); color: var(--primary); box-shadow: inset 3px 0 var(--primary); }
+.nav-link.active > span, .nav-link.active small { color: var(--primary-strong); }
+.sidebar-user { margin-top: auto; min-width: 0; display: grid; grid-template-columns: 38px minmax(0, 1fr) 10px; align-items: center; gap: 10px; padding: 15px 8px 2px; border-top: 1px solid var(--border); }
+.sidebar-user > span:nth-child(2) { min-width: 0; display: grid; line-height: 1.25; }
+.sidebar-user strong, .sidebar-user small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.sidebar-user strong { font-size: 13px; }
+.sidebar-user small { color: var(--muted); font-size: 11px; }
+.avatar { width: 38px; height: 38px; display: grid; place-items: center; border: 1px solid var(--border-strong); border-radius: 50%; background: var(--surface-high); color: var(--primary-strong); font-weight: 800; }
+.status-dot, .signal-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--positive); box-shadow: 0 0 0 4px var(--positive-soft); }
+.workspace { min-height: 100vh; margin-left: var(--sidebar); }
+.utilitybar { position: sticky; top: 0; z-index: 40; height: var(--utility); display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 0 clamp(18px, 3vw, 38px); background: color-mix(in srgb, var(--bg) 86%, transparent); border-bottom: 1px solid var(--border); backdrop-filter: blur(16px); }
+.utilitybar-start, .utility-actions, .utility-title, .utility-link { display: flex; align-items: center; }
+.utility-title { gap: 10px; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
+.utility-actions { gap: 8px; }
+.utility-actions form { margin: 0; }
+.utility-link, .icon-button { min-height: 42px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--muted); }
+.utility-link { gap: 8px; padding: 8px 10px; font-weight: 650; }
+button.utility-link { width: auto; }
+.utility-link:hover, .icon-button:hover { border-color: var(--border); background: var(--surface-high); color: var(--text); text-decoration: none; }
+.icon-button { width: 42px; display: inline-grid; place-items: center; padding: 0; }
+.theme-icon-light { display: none; }
+:root[data-theme="light"] .theme-icon-dark { display: none; }
+:root[data-theme="light"] .theme-icon-light { display: block; }
+.nav-scrim { display: none; }
+.main-content { width: 100%; max-width: 1600px; margin: 0 auto; padding: 34px clamp(18px, 3.2vw, 52px) 72px; }
-.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 28px; }
-.page-header h1, .job-hero h1 { margin: 0; font-size: clamp(30px, 4vw, 44px); line-height: 1.08; letter-spacing: -.035em; }
-.page-header p { margin: 8px 0 0; color: var(--muted); max-width: 780px; }
-.eyebrow { margin: 0 0 8px !important; color: var(--primary) !important; text-transform: uppercase; letter-spacing: .12em; font-size: 12px; font-weight: 800; }
-.section-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin: 34px 0 18px; }
-.section-heading h2 { margin: 0; font-size: 23px; letter-spacing: -.02em; }
+.page-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 26px; margin-bottom: 28px; }
+.page-header h1, .job-hero h1 { margin: 0; font-size: clamp(30px, 3.7vw, 48px); line-height: 1.06; letter-spacing: -.042em; }
+.page-header p { max-width: 760px; margin: 9px 0 0; color: var(--muted); }
+.eyebrow { margin: 0 0 9px !important; color: var(--primary) !important; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .13em; }
+.page-actions { display: flex; flex-wrap: wrap; gap: 9px; }
+.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 20px; margin: 34px 0 17px; }
+.section-heading h2 { margin: 0; font-size: 23px; letter-spacing: -.025em; }
.section-heading p { margin: 5px 0 0; color: var(--muted); }
-.title-row { display: flex; align-items: center; gap: 10px; }
+.section-kicker { color: var(--primary); font: 11px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; letter-spacing: .11em; }
+.title-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
-.button {
- min-height: 40px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 8px 14px;
- border: 1px solid transparent;
- border-radius: 10px;
- background: transparent;
- color: var(--text);
- font-weight: 700;
- line-height: 1.2;
- text-decoration: none;
-}
+.button { min-height: 42px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 8px 14px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--text); font-weight: 750; line-height: 1.2; text-decoration: none; }
.button:hover { text-decoration: none; transform: translateY(-1px); }
-.button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, a:focus-visible { outline: 3px solid color-mix(in srgb, var(--primary) 38%, transparent); outline-offset: 2px; }
-.button-primary { background: var(--primary); color: white; box-shadow: 0 7px 16px rgba(70,91,216,.2); }
+.button:focus-visible, .icon-button:focus-visible, .utility-link:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, summary:focus-visible, a:focus-visible { outline: 3px solid color-mix(in srgb, var(--primary) 40%, transparent); outline-offset: 3px; }
+.button-primary { background: var(--primary); color: var(--primary-ink); box-shadow: 0 8px 22px color-mix(in srgb, var(--primary) 16%, transparent); }
.button-primary:hover { background: var(--primary-strong); }
-.button-secondary { background: var(--surface); border-color: var(--border); }
-.button-secondary:hover, .button-ghost:hover { background: var(--surface-2); }
-.button-ghost { color: var(--muted); }
-.button-link { color: var(--primary); background: var(--primary-soft); }
+.button-secondary { background: var(--surface-high); border-color: var(--border); }
+.button-secondary:hover, .button-ghost:hover { background: var(--surface-bright); border-color: var(--border-strong); }
+.button-ghost { border-color: var(--border); color: var(--text-soft); }
+.button-link { color: var(--primary-strong); background: var(--primary-soft); border-color: color-mix(in srgb, var(--primary) 20%, var(--border)); }
+.button-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, var(--border)); }
.button-block { width: 100%; }
-.button-small { min-height: 32px; padding: 5px 9px; font-size: 13px; }
-.icon-button { width: 40px; height: 40px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); color: var(--text); }
+.button-small { min-height: 34px; padding: 5px 10px; font-size: 13px; }
-.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
-.stat-card, .panel, .job-card, .auth-card {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- box-shadow: var(--shadow);
-}
-.stat-card { min-height: 130px; display: grid; align-content: center; padding: 22px; }
-.stat-card span, .stat-card small { color: var(--muted); }
-.stat-card strong { margin: 2px 0; font-size: 34px; line-height: 1; letter-spacing: -.04em; }
-.panel { padding: clamp(20px, 3vw, 30px); margin-bottom: 20px; }
-.panel > h2:first-child { margin-top: 0; }
+.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
+.stat-card, .panel, .job-card, .auth-card, .filter-bar, .table-wrap, .source-card { background: color-mix(in srgb, var(--surface) 95%, transparent); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); }
+.stat-card { position: relative; min-height: 126px; display: grid; align-content: center; gap: 2px; padding: 20px; overflow: hidden; }
+.stat-card::after { content: ""; position: absolute; right: -38px; bottom: -48px; width: 110px; height: 110px; border: 1px solid color-mix(in srgb, var(--primary) 12%, transparent); border-radius: 50%; }
+.stat-card span, .stat-card small { position: relative; z-index: 1; color: var(--muted); }
+.stat-card strong { position: relative; z-index: 1; margin: 2px 0; font-size: 34px; line-height: 1; letter-spacing: -.045em; }
+.stat-card.signal { border-color: color-mix(in srgb, var(--primary) 35%, var(--border)); box-shadow: var(--signal-glow); }
+.kpi-label { font-size: 10px; text-transform: uppercase; letter-spacing: .09em; }
+.panel { padding: clamp(19px, 2.5vw, 30px); margin-bottom: 18px; }
+.panel > h2:first-child, .panel > h3:first-child { margin-top: 0; }
+.panel-subtle { background: var(--surface-low); box-shadow: none; }
-.job-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
-.job-card { padding: 22px; display: flex; flex-direction: column; gap: 16px; }
-.job-card-head { display: grid; grid-template-columns: auto 1fr; gap: 15px; align-items: flex-start; }
-.job-card h3 { margin: 7px 0 2px; font-size: 21px; line-height: 1.25; }
-.job-card h3 a { color: var(--text); }
+.dashboard-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 20px; align-items: start; }
+.dashboard-rail { position: sticky; top: calc(var(--utility) + 20px); }
+.radar-summary { position: relative; overflow: hidden; background: radial-gradient(circle at 88% 10%, var(--primary-soft), transparent 35%), var(--surface); }
+.radar-summary::after { content: ""; position: absolute; right: -50px; top: -55px; width: 190px; height: 190px; border: 1px solid color-mix(in srgb, var(--primary) 18%, transparent); border-radius: 50%; box-shadow: inset 0 0 0 38px color-mix(in srgb, var(--primary) 2%, transparent), inset 0 0 0 74px color-mix(in srgb, var(--primary) 3%, transparent); pointer-events: none; }
+.radar-summary > * { position: relative; z-index: 1; }
+.rail-list { display: grid; gap: 10px; margin: 16px 0 0; }
+.rail-item { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px 0; border-top: 1px solid var(--border); }
+.rail-item:first-child { border-top: 0; }
+.rail-item span { color: var(--muted); }
+
+.job-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 15px; }
+.job-card { position: relative; display: flex; flex-direction: column; gap: 15px; padding: 20px; transition: border-color .16s ease, transform .16s ease, box-shadow .16s ease; }
+.job-card:hover { border-color: var(--border-strong); transform: translateY(-2px); }
+.job-card.strong-match { border-color: color-mix(in srgb, var(--primary) 38%, var(--border)); box-shadow: var(--signal-glow); background: linear-gradient(145deg, var(--primary-soft), transparent 45%), var(--surface); }
+.job-card-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 15px; align-items: start; }
+.job-card h2, .job-card h3 { margin: 6px 0 3px; font-size: 20px; line-height: 1.24; letter-spacing: -.02em; }
+.job-card h2 a, .job-card h3 a { color: var(--text); }
.job-card p { margin: 0; color: var(--muted); }
-.score-ring {
- width: 58px;
- height: 58px;
- display: grid;
- place-items: center;
- border-radius: 50%;
- border: 6px solid var(--primary-soft);
- box-shadow: inset 0 0 0 2px var(--primary);
- color: var(--primary-strong);
- font-size: 18px;
- font-weight: 850;
-}
-.job-meta { display: flex; flex-wrap: wrap; gap: 8px; color: var(--muted); font-size: 13px; }
-.job-meta span { padding: 5px 8px; border-radius: 7px; background: var(--surface-2); }
+.score-block { min-width: 72px; display: grid; justify-items: end; }
+.score-block strong { color: var(--primary); font-size: 27px; line-height: 1; letter-spacing: -.04em; }
+.score-block small { margin-top: 5px; color: var(--muted); font: 9px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; letter-spacing: .06em; }
+.job-meta { display: flex; flex-wrap: wrap; gap: 7px; color: var(--muted); font-size: 11px; }
+.job-meta span { display: inline-flex; align-items: center; gap: 5px; padding: 5px 7px; border-radius: 4px; background: var(--surface-high); border: 1px solid color-mix(in srgb, var(--border) 70%, transparent); }
+.job-meta .icon { width: 13px; height: 13px; }
.job-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: auto; }
-.pill { display: inline-flex; align-items: center; width: fit-content; padding: 4px 8px; border-radius: 999px; background: var(--surface-2); color: var(--muted); font-size: 12px; font-weight: 750; }
-.pill-strong, .pill-allow { background: var(--positive-soft); color: var(--positive); }
-.pill-possible, .pill-review { background: var(--warning-soft); color: var(--warning); }
-.pill-hidden, .pill-deny { background: var(--danger-soft); color: var(--danger); }
+.pill { display: inline-flex; align-items: center; gap: 6px; width: fit-content; padding: 4px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-high); color: var(--muted); font-size: 10px; font-weight: 750; text-transform: uppercase; letter-spacing: .04em; }
+.pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
+.pill-strong, .pill-allow, .pill-active, .pill-success, .pill-offer { background: var(--positive-soft); border-color: color-mix(in srgb, var(--positive) 24%, var(--border)); color: var(--positive); }
+.pill-possible, .pill-review, .pill-uncertain, .pill-partial, .pill-interview { background: var(--warning-soft); border-color: color-mix(in srgb, var(--warning) 24%, var(--border)); color: var(--warning); }
+.pill-hidden, .pill-deny, .pill-failed, .pill-quarantined, .pill-rejected { background: var(--danger-soft); border-color: color-mix(in srgb, var(--danger) 24%, var(--border)); color: var(--danger); }
+.pill-new, .pill-applied, .pill-trial { background: var(--primary-soft); border-color: color-mix(in srgb, var(--primary) 24%, var(--border)); color: var(--primary-strong); }
.compact-list { margin: 0; padding-left: 20px; }
-.compact-list li + li { margin-top: 5px; }
+.compact-list li + li { margin-top: 6px; }
.compact-list.positive li::marker { color: var(--positive); }
.compact-list.caution li::marker { color: var(--warning); }
-.attention { padding: 11px 13px; border-radius: 9px; background: var(--warning-soft); color: var(--warning) !important; font-size: 13px; }
+.attention { padding: 10px 12px; border-left: 3px solid var(--warning); border-radius: 4px; background: var(--warning-soft); color: var(--warning) !important; font-size: 13px; }
-.filter-bar { display: flex; gap: 10px; padding: 14px; margin-bottom: 18px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
+.filter-bar { display: flex; align-items: end; gap: 10px; padding: 14px; margin-bottom: 18px; box-shadow: none; }
+.filter-bar label { display: grid; gap: 5px; }
.filter-bar .grow { flex: 1; }
-input, select, textarea {
- width: 100%;
- min-height: 42px;
- padding: 9px 11px;
- border: 1px solid var(--border);
- border-radius: 9px;
- background: var(--surface);
- color: var(--text);
-}
+.field-label { color: var(--muted); font: 10px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; letter-spacing: .07em; }
+input, select, textarea { width: 100%; min-height: 43px; padding: 9px 11px; border: 1px solid var(--border); border-radius: 5px; background: var(--surface-low); color: var(--text); }
+input::placeholder, textarea::placeholder { color: color-mix(in srgb, var(--muted) 78%, transparent); }
textarea { resize: vertical; }
-.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
+.table-wrap { overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; min-width: 760px; }
-.data-table th { text-align: left; padding: 12px 15px; background: var(--surface-2); color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
-.data-table td { padding: 15px; border-top: 1px solid var(--border); vertical-align: middle; }
-.data-table td small, .table-title + small { display: block; color: var(--muted); margin-top: 3px; }
+.data-table th { text-align: left; padding: 12px 15px; background: var(--surface-low); color: var(--muted); font: 10px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; letter-spacing: .07em; }
+.data-table td { padding: 14px 15px; border-top: 1px solid var(--border); vertical-align: middle; }
+.data-table tbody tr:hover { background: var(--surface-low); }
+.data-table td small, .table-title + small { display: block; margin-top: 3px; color: var(--muted); }
.table-title { color: var(--text); font-weight: 760; }
-.pagination { display: flex; justify-content: center; gap: 18px; padding: 20px; }
+.pagination { display: flex; justify-content: center; align-items: center; gap: 18px; padding: 22px; }
-.detail-layout { display: grid; grid-template-columns: minmax(0, 1fr) 310px; gap: 22px; align-items: start; }
-.back-link { display: inline-block; margin-bottom: 15px; color: var(--muted); }
-.job-hero { display: flex; justify-content: space-between; gap: 24px; padding: clamp(22px, 4vw, 38px); margin-bottom: 20px; border-radius: var(--radius); background: linear-gradient(140deg, var(--surface), var(--primary-soft)); border: 1px solid var(--border); }
-.hero-subtitle { margin: 10px 0 14px; color: var(--muted); font-size: 17px; }
-.hero-score { min-width: 132px; height: 132px; display: grid; place-items: center; align-content: center; border-radius: 50%; background: var(--surface); border: 8px solid var(--primary); box-shadow: var(--shadow); }
-.hero-score strong { font-size: 36px; line-height: 1; }
-.hero-score span, .hero-score small { color: var(--muted); font-size: 12px; }
-.sticky-card { position: sticky; top: 92px; padding: 22px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); }
-.sticky-card h2 { margin-top: 0; }
+.detail-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 20px; align-items: start; }
+.back-link { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 15px; color: var(--muted); }
+.job-hero { position: relative; overflow: hidden; display: flex; justify-content: space-between; gap: 28px; padding: clamp(24px, 4vw, 40px); margin-bottom: 18px; border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border)); border-radius: var(--radius); background: radial-gradient(circle at 88% 25%, var(--primary-soft), transparent 32%), linear-gradient(145deg, var(--surface), var(--surface-low)); box-shadow: var(--signal-glow); }
+.hero-subtitle { margin: 11px 0 15px; color: var(--text-soft); font-size: 17px; }
+.hero-score { min-width: 136px; display: grid; justify-items: end; align-content: start; }
+.hero-score strong { color: var(--primary); font-size: 54px; line-height: .95; letter-spacing: -.06em; }
+.hero-score span { margin-top: 8px; color: var(--primary-strong); font: 10px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; letter-spacing: .08em; }
+.hero-score small { margin-top: 3px; color: var(--muted); }
+.sticky-card { position: sticky; top: calc(var(--utility) + 20px); padding: 20px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); }
+.sticky-card h2 { margin: 0 0 4px; }
+.sticky-card > p { margin-top: 0; color: var(--muted); }
.stack-actions { display: grid; gap: 9px; }
-.hide-form { display: grid; gap: 8px; }
+.hide-form { display: grid; gap: 8px; padding-top: 9px; border-top: 1px solid var(--border); }
.hide-form label { color: var(--muted); font-size: 12px; }
-.score-components { display: grid; gap: 12px; margin: 18px 0 24px; }
+.score-components { display: grid; gap: 13px; margin: 18px 0 25px; }
.score-components > div { display: grid; grid-template-columns: 120px minmax(120px, 1fr) 52px; gap: 12px; align-items: center; }
.score-components strong { text-align: right; }
-.progress { height: 9px; overflow: hidden; border-radius: 999px; background: var(--surface-2); }
-.progress i { display: block; height: 100%; min-width: 2px; border-radius: inherit; background: var(--primary); }
-.two-columns { display: grid; grid-template-columns: repeat(2, 1fr); gap: 22px; }
-.prose { max-width: 82ch; }
-.prose h2, .prose h3, .prose h4 { margin-top: 1.5em; }
-.prose p, .prose li { color: color-mix(in srgb, var(--text) 88%, var(--muted)); }
-.definition-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
-.definition-grid div { padding: 12px; border-radius: 10px; background: var(--surface-2); }
-.definition-grid dt { color: var(--muted); font-size: 12px; }
-.definition-grid dd { margin: 3px 0 0; font-weight: 700; }
+.progress { height: 7px; overflow: hidden; border-radius: 999px; background: var(--surface-high); }
+.progress i { display: block; height: 100%; min-width: 2px; border-radius: inherit; background: linear-gradient(90deg, var(--primary), var(--indigo)); }
+.two-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; }
+.prose { max-width: 82ch; color: var(--text-soft); }
+.prose h2, .prose h3, .prose h4 { margin-top: 1.5em; color: var(--text); }
+.definition-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
+.definition-grid div { padding: 12px; border: 1px solid var(--border); border-radius: 5px; background: var(--surface-low); }
+.definition-grid dt { color: var(--muted); font: 10px ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
+.definition-grid dd { margin: 4px 0 0; font-weight: 700; }
.source-list { list-style: none; padding: 0; }
.source-list li { display: flex; justify-content: space-between; gap: 16px; padding: 11px 0; border-top: 1px solid var(--border); }
.source-list span { color: var(--muted); font-size: 12px; }
-.card-list { display: grid; gap: 14px; }
+.pipeline { display: grid; grid-template-columns: repeat(6, minmax(210px, 1fr)); gap: 12px; overflow-x: auto; padding: 1px 1px 14px; scroll-snap-type: x proximity; }
+.pipeline-column { min-height: 320px; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-low); scroll-snap-align: start; }
+.pipeline-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 3px 3px 12px; }
+.pipeline-head h2 { margin: 0; font-size: 14px; }
+.pipeline-count { min-width: 25px; height: 25px; display: grid; place-items: center; border: 1px solid var(--border); border-radius: 50%; color: var(--muted); font-size: 11px; }
+.pipeline-cards { display: grid; gap: 9px; }
+.pipeline-card { padding: 14px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); box-shadow: 0 8px 20px rgba(0, 0, 0, .08); }
+.pipeline-card h3 { margin: 8px 0 3px; font-size: 15px; line-height: 1.3; }
+.pipeline-card p { margin: 0 0 12px; color: var(--muted); font-size: 12px; }
+.pipeline-empty { padding: 20px 8px; color: var(--muted); font-size: 12px; text-align: center; border: 1px dashed var(--border); border-radius: 5px; }
+
+.card-list { display: grid; gap: 12px; }
.profile-card, .application-card { display: flex; justify-content: space-between; align-items: center; gap: 24px; margin: 0; }
.profile-card h2, .application-card h2 { margin: 5px 0; }
-.form-panel { max-width: 980px; }
-.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
-.form-grid label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; font-weight: 650; }
+.form-panel { max-width: 1100px; }
+.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 17px; }
+.form-grid label { display: grid; align-content: start; gap: 6px; color: var(--muted); font-size: 13px; font-weight: 650; }
.form-grid label > input, .form-grid label > select, .form-grid label > textarea { color: var(--text); font-weight: 400; }
+.form-section { grid-column: 1 / -1; margin: 12px 0 -2px; padding-top: 18px; border-top: 1px solid var(--border); }
+.form-section h2 { margin: 0; font-size: 17px; }
+.form-section p { margin: 4px 0 0; color: var(--muted); font-size: 12px; }
.span-2 { grid-column: 1 / -1; }
.field-error { color: var(--danger); }
-.form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; }
+.form-actions { position: sticky; bottom: 12px; z-index: 5; display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; padding: 12px; border: 1px solid var(--border); border-radius: 7px; background: color-mix(in srgb, var(--surface) 92%, transparent); backdrop-filter: blur(12px); }
+
+.source-overview { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-bottom: 20px; }
+.source-card { overflow: hidden; box-shadow: none; }
+.source-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 18px; }
+.source-identity { min-width: 0; display: flex; gap: 13px; }
+.source-icon { width: 42px; height: 42px; display: grid; place-items: center; flex: 0 0 auto; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-high); color: var(--primary); }
+.source-identity h2 { margin: 0; font-size: 17px; }
+.source-identity p { margin: 4px 0 0; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
+.source-actions { display: flex; align-items: center; gap: 10px; }
+.source-details { padding: 18px; border-top: 1px solid var(--border); background: var(--surface-low); }
+.source-details-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
+.technical-log { max-height: 180px; overflow: auto; padding: 14px; border: 1px solid var(--border); border-radius: 5px; background: var(--bg-deep); color: var(--muted); font: 11px/1.6 ui-monospace, SFMono-Regular, Consolas, monospace; overflow-wrap: anywhere; }
+.disclosure-icon { transition: transform .16s ease; }
+[aria-expanded="true"] .disclosure-icon { transform: rotate(180deg); }
+.import-layout { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.messages { display: grid; gap: 8px; margin-bottom: 18px; }
-.message { padding: 12px 14px; border-radius: 10px; background: var(--primary-soft); color: var(--primary-strong); border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--border)); }
-.message.success { background: var(--positive-soft); color: var(--positive); }
-.message.warning { background: var(--warning-soft); color: var(--warning); }
-.message.error { background: var(--danger-soft); color: var(--danger); }
-.empty-state { grid-column: 1 / -1; padding: 54px 24px; text-align: center; border: 1px dashed var(--border); border-radius: var(--radius); background: var(--surface); }
+.message { padding: 12px 14px; border: 1px solid color-mix(in srgb, var(--primary) 24%, var(--border)); border-left: 3px solid var(--primary); border-radius: 5px; background: var(--primary-soft); color: var(--primary-strong); }
+.message.success { background: var(--positive-soft); border-color: color-mix(in srgb, var(--positive) 25%, var(--border)); border-left-color: var(--positive); color: var(--positive); }
+.message.warning { background: var(--warning-soft); border-color: color-mix(in srgb, var(--warning) 25%, var(--border)); border-left-color: var(--warning); color: var(--warning); }
+.message.error { background: var(--danger-soft); border-color: color-mix(in srgb, var(--danger) 25%, var(--border)); border-left-color: var(--danger); color: var(--danger); }
+.empty-state { grid-column: 1 / -1; padding: 48px 24px; text-align: center; border: 1px dashed var(--border-strong); border-radius: var(--radius); background: var(--surface-low); }
.empty-state h2 { margin-bottom: 6px; }
-.empty-state p { color: var(--muted); max-width: 600px; margin: 0 auto 16px; }
-.empty-icon { font-size: 40px; color: var(--primary); }
-.auth-card { width: min(460px, 100%); padding: 32px; }
+.empty-state p { max-width: 600px; margin: 0 auto 16px; color: var(--muted); }
+.empty-icon { width: 54px; height: 54px; display: grid; place-items: center; margin: 0 auto 14px; border: 1px solid var(--border); border-radius: 50%; color: var(--primary); background: var(--primary-soft); }
+.empty-icon .icon { width: 27px; height: 27px; }
+.muted { color: var(--muted); }
+.small { font-size: 12px; }
+.auth-main { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: radial-gradient(circle at 18% 14%, var(--primary-soft), transparent 32%), radial-gradient(circle at 83% 85%, rgba(104, 112, 255, .08), transparent 28%), var(--bg); }
+.auth-card { width: min(450px, 100%); padding: 30px; }
.auth-brand { display: flex; align-items: center; gap: 14px; margin-bottom: 24px; }
.auth-card h1 { margin: 0; }
.auth-card p { margin: 4px 0; color: var(--muted); }
.stack { display: grid; gap: 16px; }
.stack label { display: grid; gap: 6px; font-weight: 650; }
-.muted { color: var(--muted); }
-.small { font-size: 12px; }
-@media (max-width: 1100px) {
- .stats-grid { grid-template-columns: repeat(2, 1fr); }
- .job-grid { grid-template-columns: 1fr; }
+@media (max-width: 1180px) {
+ .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .dashboard-grid { grid-template-columns: 1fr; }
+ .dashboard-rail { position: static; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 15px; }
.detail-layout { grid-template-columns: 1fr; }
.sticky-card { position: static; }
+ .source-overview { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
-@media (max-width: 760px) {
- .mobile-only { display: inline-grid; place-items: center; }
- .topbar { padding: 10px 14px; }
- .brand small, .topbar-actions .button:first-child { display: none; }
- .app-shell { grid-template-columns: 1fr; }
- .sidebar { position: fixed; inset: 70px auto 0 0; z-index: 40; width: min(86vw, 300px); height: calc(100vh - 70px); transform: translateX(-105%); transition: transform .2s ease; box-shadow: var(--shadow); }
- .sidebar.open { transform: translateX(0); }
- .main-content { padding: 24px 15px 50px; }
- .page-header, .profile-card, .application-card, .job-hero { flex-direction: column; }
- .page-header .button { width: 100%; }
- .stats-grid, .two-columns, .form-grid, .definition-grid { grid-template-columns: 1fr; }
- .filter-bar { flex-direction: column; }
- .span-2 { grid-column: auto; }
- .hero-score { width: 105px; height: 105px; min-width: 105px; }
- .score-components > div { grid-template-columns: 94px 1fr 40px; }
+@media (max-width: 900px) {
+ .mobile-only { display: inline-grid !important; }
+ .sidebar { visibility: hidden; transform: translateX(-105%); transition: transform .18s ease, visibility 0s linear .18s; }
+ .workspace { margin-left: 0; }
+ .nav-open .sidebar { visibility: visible; transform: translateX(0); transition-delay: 0s; }
+ .nav-scrim { position: fixed; inset: 0; z-index: 50; width: 100%; height: 100%; border: 0; background: rgba(1, 8, 17, .62); backdrop-filter: blur(2px); }
+ .nav-open .nav-scrim { display: block; }
+ .utilitybar { padding: 0 16px; }
+ .main-content { padding-top: 26px; }
+ .job-grid { grid-template-columns: 1fr; }
+ .dashboard-rail { grid-template-columns: 1fr 1fr; }
+ .pipeline { grid-template-columns: repeat(6, 245px); }
+}
+
+@media (max-width: 680px) {
+ .desktop-label { display: none; }
+ .utility-link { width: 42px; justify-content: center; padding: 0; border: 1px solid transparent; }
+ .main-content { padding: 22px 14px 54px; }
+ .page-header, .profile-card, .application-card, .job-hero { align-items: stretch; flex-direction: column; }
+ .page-header .button, .page-actions { width: 100%; }
+ .page-actions .button { flex: 1; }
+ .stats-grid, .two-columns, .form-grid, .definition-grid, .source-overview, .source-details-grid, .import-layout, .dashboard-rail { grid-template-columns: 1fr; }
+ .filter-bar { align-items: stretch; flex-direction: column; }
+ .span-2, .form-section { grid-column: auto; }
+ .job-card-head { grid-template-columns: 1fr; }
+ .score-block { justify-items: start; }
+ .job-hero { gap: 20px; }
+ .hero-score { min-width: 0; justify-items: start; }
+ .hero-score strong { font-size: 46px; }
+ .score-components > div { grid-template-columns: 86px minmax(80px, 1fr) 40px; font-size: 12px; }
.source-list li { flex-direction: column; gap: 3px; }
+ .source-card-head { flex-direction: column; }
+ .source-actions { width: 100%; justify-content: space-between; }
+ .form-actions { bottom: 8px; }
+}
+
+@media (max-width: 420px) {
+ .utility-title span:last-child { display: none; }
+ .stats-grid { gap: 9px; }
+ .stat-card { min-height: 112px; padding: 16px; }
+ .stat-card strong { font-size: 29px; }
+ .page-header h1, .job-hero h1 { font-size: 30px; }
+ .panel, .job-card { padding: 17px; }
+ .job-actions form, .job-actions .button { flex: 1 1 auto; }
}
@media (prefers-reduced-motion: reduce) {
diff --git a/static/js/app.js b/static/js/app.js
index 55ca1fb..5c7ad48 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -1,23 +1,54 @@
(() => {
- const toggle = document.querySelector('[data-nav-toggle]');
- const sidebar = document.querySelector('[data-sidebar]');
- if (!toggle || !sidebar) return;
+ const root = document.documentElement;
+ let storedTheme = null;
+ try {
+ storedTheme = localStorage.getItem("vacatureradar-theme");
+ } catch {
+ // Private or embedded browser contexts may disable persistent storage.
+ }
+ if (storedTheme === "light" || storedTheme === "dark") root.dataset.theme = storedTheme;
- const close = () => {
- sidebar.classList.remove('open');
- toggle.setAttribute('aria-expanded', 'false');
- };
-
- toggle.addEventListener('click', () => {
- const open = sidebar.classList.toggle('open');
- toggle.setAttribute('aria-expanded', String(open));
- });
- document.addEventListener('keydown', (event) => {
- if (event.key === 'Escape') close();
- });
- document.addEventListener('click', (event) => {
- if (sidebar.classList.contains('open') && !sidebar.contains(event.target) && !toggle.contains(event.target)) {
- close();
+ const themeToggle = document.querySelector("[data-theme-toggle]");
+ themeToggle?.addEventListener("click", () => {
+ const theme = root.dataset.theme === "light" ? "dark" : "light";
+ root.dataset.theme = theme;
+ try {
+ localStorage.setItem("vacatureradar-theme", theme);
+ } catch {
+ // The selected theme still applies for the current page.
}
+ themeToggle.setAttribute("aria-label", theme === "light" ? "Donker thema activeren" : "Licht thema activeren");
+ });
+
+ const frame = document.querySelector("[data-app-frame]");
+ const sidebar = document.querySelector("[data-sidebar]");
+ const toggle = document.querySelector("[data-nav-toggle]");
+ const closeButtons = document.querySelectorAll("[data-nav-close], [data-nav-scrim]");
+ let returnFocus = null;
+
+ const closeNavigation = () => {
+ frame?.classList.remove("nav-open");
+ toggle?.setAttribute("aria-expanded", "false");
+ if (returnFocus instanceof HTMLElement) returnFocus.focus();
+ };
+ toggle?.addEventListener("click", () => {
+ returnFocus = document.activeElement;
+ frame?.classList.add("nav-open");
+ toggle.setAttribute("aria-expanded", "true");
+ sidebar?.querySelector("a, button")?.focus();
+ });
+ closeButtons.forEach((button) => button.addEventListener("click", closeNavigation));
+ document.addEventListener("keydown", (event) => {
+ if (event.key === "Escape") closeNavigation();
+ });
+
+ document.querySelectorAll("[data-disclosure]").forEach((button) => {
+ button.addEventListener("click", () => {
+ const target = document.getElementById(button.getAttribute("aria-controls"));
+ if (!target) return;
+ const expanded = button.getAttribute("aria-expanded") === "true";
+ button.setAttribute("aria-expanded", String(!expanded));
+ target.hidden = expanded;
+ });
});
})();
diff --git a/templates/applications/edit.html b/templates/applications/edit.html
index fb6103f..5017a1a 100644
--- a/templates/applications/edit.html
+++ b/templates/applications/edit.html
@@ -1,3 +1,7 @@
{% extends "base.html" %}
{% block title %}Sollicitatiedossier · VacatureRadar{% endblock %}
-{% block content %} Sollicitatiedossier {{ object.job.employer_name }}{{ object.job.original_title }}
Sollicitatiedossier
{{ object.job.employer_name }} · alleen zichtbaar voor jouw account
De export bevat de opgeslagen snapshot en tijdlijn. Verwijderen wist alleen jouw dossier; de onderliggende vacature blijft behouden.
Persoonlijke opvolging
Vacaturesnapshots blijven bewaard wanneer de bron offline gaat.
{{ application.job.employer_name }} · {{ application.job.raw_location }}
Markeer een vacature als Gesolliciteerd om automatisch een dossier te maken.
Persoonlijke opvolging
Vacaturesnapshots blijven beschikbaar wanneer de oorspronkelijke bron offline gaat.
{{ application.job.employer_name }}{% if application.job.raw_location %} · {{ application.job.raw_location }}{% endif %}
Markeer een vacature als Gesolliciteerd om automatisch een persoonlijk dossier te maken.
Vacatures bekijkenDagelijkse selectie
Alleen nieuwe en relevante vacatures die aandacht verdienen.
Intelligence cockpit
Je persoonlijke radar brengt alleen actuele signalen en concrete volgende stappen samen.
Gerangschikt op persoonlijke matchscore; confidence blijft afzonderlijk zichtbaar.
Aandachtspunt: {{ score.concerns.0 }}
{% endif %} -Matchscore en dataconfidence blijven bewust afzonderlijk zichtbaar.
Aandachtspunt: {{ score.concerns.0 }}
{% endif %} +Importeer vacaturedata of activeer een toegestane bron. De eerste betrouwbare resultaten verschijnen hier automatisch.
Bronnen beherenImporteer de meegeleverde fixture of activeer een toegestane bron. De eerste resultaten verschijnen hier automatisch.
make demo{{ job.get_status_display }}
{{ job.employer_name }} · {{ job.raw_location|default:"Locatie onbekend" }}
De inhoudelijke score en de betrouwbaarheid worden apart behandeld.
{{ job.description_text|linebreaksbr }}
{{ job.get_status_display }}{% if job.date_posted %} · gepubliceerd {{ job.date_posted|date:"d/m/Y" }}{% endif %}
{{ job.employer_name }}{% if job.raw_location %} · {{ job.raw_location }}{% endif %}
De inhoudelijke match en betrouwbaarheid worden afzonderlijk beoordeeld.
Deze vacature heeft nog geen score voor je actieve zoekprofiel. De broninformatie en vacaturetekst blijven wel beschikbaar.
{{ job.description_text|linebreaksbr }}
Geen vacatureomschrijving beschikbaar.
{% endif %}Alle resultaten
Doorzoek actieve en gearchiveerde vacatureclusters.
Nieuwe matches & archief
Doorzoek actuele en gearchiveerde vacatures zonder de herkomst of status uit het oog te verliezen.
| Vacature | Locatie | Werkmodel | Status | Gevonden |
|---|---|---|---|---|
| {{ job.original_title }}{{ job.employer_name }} | {{ job.raw_location|default:"—" }} | {{ job.get_workplace_type_display }} | {{ job.get_status_display }} | {{ job.first_seen|date:"d/m/Y" }} |
| Geen vacatures gevonden voor deze filters. | ||||
Vereiste skills: {{ job.skills_required|slice:":3"|join:", " }}
{% endif %} + +Pas de zoekterm of filters aan. Gearchiveerde resultaten verschijnen wanneer je de statusfilter leegmaakt.
Zoekprofiel versie {{ object.version }}
Wijzig alleen persoonlijke keuzes; bronlogica blijft automatisch.
Zoekprofiel · versie {{ object.version }}
Je voorkeuren sturen ranking en uitsluiting; bronbeleid en veiligheidsregels blijven automatisch bewaakt.
Persoonlijke regels
Harde grenzen veranderen nooit automatisch; zachte gewichten kunnen gecontroleerd leren.
{{ profile.desired_titles|join:", "|default:"Nog geen functietitels" }}
Voer de bootstrapopdracht uit om het standaardprofiel aan te maken.
Persoonlijke regels
Harde grenzen veranderen nooit automatisch; zachte voorkeuren kunnen alleen gecontroleerd leren.
{{ profile.desired_titles|join:", "|default:"Nog geen functietitels" }}
Voer de bootstrapopdracht uit om het standaardprofiel aan te maken.
Open je persoonlijke vacatureselectie.
Intelligence cockpit
Open je persoonlijke vacatureselectie.
De applicatie is ontworpen voor lokaal gebruik via LAN of VPN.
+ +Beveiligde lokale toegang via je eigen netwerk, VPN of reverse proxy.