Andrej Janchevski commited on
Commit
0f7b533
Β·
1 Parent(s): 95c7d01

docs(plan): add frontend homepage and CV page plan

Browse files

- Bootstraps Vue 3 + Vite + Semantic UI (fomantic-ui-css) frontend at
src/frontend/ with responsive Home and CV pages
- Shared animated graph background sampled from the COINs 3- and 4-node
motif enumeration, with prefers-reduced-motion fallback
- Homepage integrates NELL Fact of the Day (sample-triples) and a
System Status widget aggregating /health, /, and /methods
- Backend adds subject_label/relation_label/object_label to
sample-triples using existing clean_entity_name/clean_relation_name
helpers; api.yaml, docs/postman, and backend README updated
- CV content sourced verbatim from docs/Profile.pdf (LinkedIn export)
- Dark mode, mobile sidebar, rabbit-emoji favicon, logos for EPFL,
Swisscom, Attercop, CERN, Data Masters, Endava, FCSE, Isomorphic
Labs, Google DeepMind

Files changed (1) hide show
  1. .claude/plans/frontend_home_cv.md +248 -0
.claude/plans/frontend_home_cv.md ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Frontend Bootstrap: Homepage and CV Page
2
+
3
+ Bootstraps the Vue.js + Semantic UI frontend of the PhD website with two responsive pages (Homepage, CV), dark mode, a shared animated graph background sampled from the COINs motif enumeration, and the first live backend integrations: **NELL fact of the day** (sample-triples) plus a **system status** widget driven by the health endpoints.
4
+
5
+ ## Context
6
+
7
+ The backend (Django REST API at `src/backend/`) is working and exposes the demo endpoints, a random-triple endpoint per KG dataset, and three health/discovery endpoints (`/api/v1/health`, `/api/v1/`, `/api/v1/methods`). No frontend exists yet β€” `src/frontend/` is empty. `CLAUDE.md` pins the stack to **Vue.js + Semantic UI ONLY** and prescribes a green-themed background of "drawings of graphs (nodes and edges) moving around like molecules". The demo pages (COINs, MultiProxAn, KG-anomaly) are out of scope here β€” only Homepage and CV ship now, with placeholder cards teasing the future demos.
8
+
9
+ **`docs/Profile.pdf` (LinkedIn export) is the single source of truth for all Work Experience and Education text on `/cv`** β€” every bullet, title, date, and location is transcribed verbatim from it into `src/frontend/src/data/cv.js`. The headshot at `docs/picture.jpg` is the avatar. Other sections (Profile summary, Skills, Languages, Projects, Publications, References, Honors & Awards) are also taken from `docs/Profile.pdf`. The COINs research code at `src/research/COINs-KGGeneration/graph_analysis/metrics.py` (`get_all_motifs`, lines 374–394) enumerates **13 directed 3-node motifs and 199 directed 4-node motifs** β€” these drive the animated background.
10
+
11
+ ### Why Vite
12
+
13
+ **Vite** is a modern frontend build tool (dev server + production bundler). It serves the source over native ES modules in dev β€” no bundling β€” so the dev server starts in <1 s and HMR is near-instant regardless of project size. For production it bundles with Rollup (tree-shaking, code-splitting). It is the recommended stack for Vue 3 (replaces `vue-cli`/webpack, which the older `ti-dashboard-master` CERN project used).
14
+
15
+ Why we need a build tool at all: `.vue` Single-File Components are not native browser syntax β€” the `<template>` / `<script>` / `<style>` blocks must be compiled to JS. Without a build step we'd be limited to runtime template compilation, no SFCs, no path aliases, no env files, no asset pipeline. Vite gives us all that with effectively zero config.
16
+
17
+ ### Inspiration from prior Vue projects
18
+
19
+ - **`ti-dashboard-master/frontend`** (CERN TI dashboard β€” Vue 2 + vue-cli + TypeScript + Semantic UI + Vuex + vue-router): clean `components/` (NavBar, DatePicker, EventsTable, OperatorInfo, UserSearch, Vistar, CallsChart) vs `views/` (Dashboard.vue); single Vuex store; router-driven layout. We mirror the small-prop-driven-component philosophy, on Vue 3 + Vite.
20
+ - **`github.com/Bani57/finkiScholar`** (FOSS coursework β€” Laravel + Vue 2 + Semantic UI + vue-router, bundled via laravel-mix). Components: App, NavBar, Home, Profile, Archive, Publish, Login, Register, DatePicker. Confirms the NavBar + view-component pattern used here, and is the canonical source for the UKIM/FINKI logo (`resources/assets/logo.png`).
21
+
22
+ ## Assumptions and Constraints
23
+
24
+ - Stack is **Vue 3 + Vite + Vue Router 4 + Pinia + Semantic UI CSS (`fomantic-ui-css`) + axios**. No Tailwind, no d3, no Three.js. Semantic is consumed as CSS classes on plain Vue templates β€” no component-wrapper library.
25
+ - Backend dev URL: `http://localhost:8000`; prod URL: `https://bani57.pythonanywhere.com`. Switched via Vite env files (`.env.development`, `.env.production`) exposing `VITE_API_BASE_URL`.
26
+ - The motif list is **static** β€” generated once from Python and committed as JSON. No runtime call to the research code.
27
+ - **Responsive design is a hard requirement**: layouts must work cleanly from 320 px up to 4K. Use Semantic's responsive grid (`computer only`, `tablet only`, `mobile only` classes) and CSS `clamp()` for typography.
28
+ - **Dark mode is in scope**: theme toggle in NavBar, persisted in `localStorage`, defaulting to system preference (`prefers-color-scheme`). Implemented via a `data-theme="dark"` attribute on `<html>` + CSS custom properties, so Semantic's components remain styled by overriding background/text vars only.
29
+ - Animations use SVG + `requestAnimationFrame`, respect `prefers-reduced-motion`. No WebGL.
30
+ - Logos are downloaded once and committed to `src/frontend/public/logos/`. If a logo is not freely redistributable, we use a text fallback (acronym in a styled badge).
31
+ - Browser target: evergreen (Chrome/Edge/Firefox/Safari latest).
32
+ - **Docs hygiene for every backend change**: when an endpoint's request/response shape, behaviour, or error codes change (or a new endpoint is added), update all three in the same commit β€” `docs/api.yaml` (OpenAPI spec), `docs/postman/` (collection + environment), and `src/backend/README.md` (endpoint table).
33
+
34
+ ## Scope
35
+
36
+ **In scope**
37
+ - `src/frontend/` Vite scaffold, build/dev scripts, frontend `README.md`.
38
+ - Shared layout: persistent navbar (Home, CV, dark-mode toggle); routed main content; shared `GraphBackground` behind all pages; rabbit-emoji favicon.
39
+ - Homepage (`/`): hero intro with avatar, education/work highlights, demo preview cards (COINs / MultiProxAn / KG-Anomaly β€” each "coming soon"), **NELL Fact of the Day** widget, **System Status** widget aggregating all three health/discovery endpoints.
40
+ - CV page (`/cv`): full mirror of the PDF using reusable components (SocialLink, WorkExperience, Education, Skill, Language, Publication, Reference, Project), plus the headshot.
41
+ - Dark mode + theme persistence.
42
+ - Mobile/tablet responsiveness for both pages.
43
+ - Motif JSON export script (one-shot, committed output).
44
+ - Logo assets for: EPFL, Swisscom, Attercop, **CERN**, **Data Masters**, **Endava**, Ss. Cyril & Methodius University (UKIM) / Faculty of Computer Science and Engineering (FCSE/FINKI), Isomorphic Labs, Google DeepMind.
45
+
46
+ **Out of scope**
47
+ - The three interactive demo pages (only teaser cards on the homepage).
48
+ - Authentication, analytics, i18n.
49
+ - Deployment config changes on PythonAnywhere (later plan).
50
+ - Unit/e2e tests β€” manual browser verification only for this first cut.
51
+
52
+ ## Design
53
+
54
+ ### Directory layout (`src/frontend/`)
55
+
56
+ ```
57
+ src/frontend/
58
+ index.html # <link rel="icon" ...> rabbit-emoji SVG data URL
59
+ package.json
60
+ vite.config.js
61
+ .env.development # VITE_API_BASE_URL=http://localhost:8000
62
+ .env.production # VITE_API_BASE_URL=https://bani57.pythonanywhere.com
63
+ README.md
64
+ public/
65
+ logos/
66
+ epfl.svg
67
+ swisscom.svg
68
+ attercop.svg
69
+ cern.svg
70
+ data-masters.svg
71
+ endava.svg
72
+ fcse.svg
73
+ ukim.svg
74
+ isomorphic-labs.svg
75
+ google-deepmind.svg
76
+ README.md # source + licence per logo
77
+ avatar.jpg # copied from docs/picture.jpg at build/setup time
78
+ favicon.svg # inline rabbit emoji
79
+ src/
80
+ main.js # app + router + pinia + fomantic-ui-css
81
+ App.vue # <NavBar/> + <GraphBackground/> + <RouterView/>
82
+ router/index.js # routes: /, /cv, scrollBehavior to top
83
+ stores/
84
+ theme.js # Pinia: { mode: 'light'|'dark', toggle(), init() }
85
+ api/
86
+ client.js # axios instance; baseURL = VITE_API_BASE_URL + '/api/v1'
87
+ coins.js # sampleTriples(datasetId, count)
88
+ health.js # getHealth(), getApiRoot(), getMethods()
89
+ data/
90
+ motifs.json # { motifs3: [[[0,1],...]], motifs4: [...] }
91
+ cv.js # full CV content (single source of truth)
92
+ components/
93
+ layout/
94
+ NavBar.vue # nav links + theme toggle
95
+ PageSection.vue # Semantic `ui container segment` wrapper
96
+ ThemeToggle.vue # sun/moon icon button
97
+ background/
98
+ GraphBackground.vue # full-viewport SVG, spawns FloatingMotif instances
99
+ FloatingMotif.vue # one small SVG motif with motion state
100
+ cv/
101
+ SocialLink.vue
102
+ WorkExperience.vue # employer, role, dates, location, bullets, logo
103
+ Education.vue # institution, degree, dates, location, gpa, logo
104
+ Skill.vue # category + tag list
105
+ Language.vue # name + 1-5 proficiency dots
106
+ Publication.vue # title, venue, date, optional link
107
+ Reference.vue # name, title, org, email
108
+ ProjectCard.vue
109
+ home/
110
+ HeroIntro.vue # avatar + name + title + bio + CTAs
111
+ DemoPreviewCard.vue # icon + title + description + "coming soon" badge
112
+ FactOfTheDay.vue # subject -> relation -> object pills + reload
113
+ SystemStatus.vue # aggregates /health, /, /methods into one card
114
+ views/
115
+ HomeView.vue
116
+ CVView.vue
117
+ NotFoundView.vue
118
+ styles/
119
+ tokens.css # CSS custom properties for both themes
120
+ theme.css # Semantic overrides + dark-mode rules
121
+ responsive.css # breakpoint helpers
122
+ ```
123
+
124
+ ### Key existing files referenced
125
+
126
+ - `src/research/COINs-KGGeneration/graph_analysis/metrics.py:374` β€” `get_all_motifs(k)` for JSON export.
127
+ - `src/backend/api/utils.py` β€” `clean_entity_name` / `clean_relation_name`; called by the extended sample-triples view.
128
+ - `src/backend/api/views/coins.py`, `src/backend/api/views/health.py` (and `api/urls.py`) β€” confirm response shapes; `coins.py` gets the additive `*_label` fields.
129
+ - `docs/Profile.pdf` β€” verbatim source for every Work Experience, Education, and other narrative section in `cv.js`. `docs/cvAndrejJanchevski.pdf` is not used for CV text.
130
+ - `docs/picture.jpg` β€” copied to `src/frontend/public/avatar.jpg`.
131
+
132
+ ### GraphBackground animation
133
+
134
+ - Full-viewport fixed SVG, `z-index: -1`, mounted once in `App.vue`.
135
+ - Spawns **N β‰ˆ 20** `FloatingMotif` instances (single tunable constant; reduced on mobile via `matchMedia('(max-width: 640px)')` to β‰ˆ 10 for performance).
136
+ - Each `FloatingMotif`: random pick from `motifs.json` (50/50 weight 3-node vs 4-node so 3-node motifs stay visible), circular layout (k points on a 20 px radius), SVG `<line>` edges with arrow markers + `<circle>` nodes, drift via `requestAnimationFrame` translating the `<g>` plus gentle rotation, wrap around viewport edges, re-randomise on wrap (motif, drift vector, scale 40–80 px).
137
+ - Color palette uses CSS variables so dark mode flips to brighter green-on-dark; light mode is translucent sea-green-on-cream.
138
+ - `prefers-reduced-motion: reduce` β†’ motifs render static at fixed positions, no rAF loop.
139
+
140
+ ### Motif data generation
141
+
142
+ - One-shot script `scripts/export_motifs.py` (new) imports `get_all_motifs(3)` and `get_all_motifs(4)` and writes `src/frontend/src/data/motifs.json` with two arrays of edge lists. Counts asserted (13 and 199). Output committed.
143
+
144
+ ### NELL Fact of the Day β€” name prettification
145
+
146
+ The COINs `/sample-triples` endpoint returns raw IDs (e.g. `concept:person:john_doe`, `concept:agentcollaborateswithagent`). The backend already exposes `clean_entity_name(name, dataset_id)` and `clean_relation_name(name, dataset_id)` in `src/backend/api/utils.py` covering all three datasets (freebase strips `/m/`, wordnet drops the POS suffix, nell strips `concept:` prefixes and takes the tail segment, with a `new`-slug edge case). Approach:
147
+
148
+ 1. **Backend**: extend the `/coins/datasets/{id}/sample-triples` response so each triple carries `subject_label`, `relation_label`, `object_label` produced by the existing `clean_entity_name` / `clean_relation_name` helpers. Keep the raw IDs too (for tooltips and downstream use). This is a small, additive change in `src/backend/api/views/coins.py` β€” no new Python logic, just a call to the existing utils. Updates `docs/api.yaml` accordingly.
149
+ 2. **Frontend**: `FactOfTheDay.vue` calls `coins.sampleTriples('nell', 1)` and renders three Semantic `ui label` pills (subject β†’ relation β†’ object) using the `*_label` fields; raw ID shown as a tooltip on hover.
150
+ 3. No frontend-side lookup table needed β€” the `nellLabels.json` export is dropped; `data/` loses that file. Falls back to the raw ID if a label field is absent (defensive, but shouldn't happen).
151
+ 4. Loading state: Semantic `ui placeholder`. Error state: muted "Could not reach the server". Reload icon button re-fetches.
152
+
153
+ ### System Status widget
154
+
155
+ Single card on the homepage that calls all three discovery endpoints in parallel on mount:
156
+
157
+ - `GET /api/v1/health` β†’ green/yellow/red dot per model (`coins`, `multiproxan`, `kg_anomaly`); shows inference lock state.
158
+ - `GET /api/v1/` β†’ API name + version (small caption).
159
+ - `GET /api/v1/methods` β†’ list of the three research methods with thesis section labels (links to the future demo pages β€” currently inert).
160
+
161
+ Aggregation in `api/health.js`:
162
+
163
+ ```js
164
+ export async function fetchSystemStatus() {
165
+ const [health, root, methods] = await Promise.all([
166
+ client.get('/health'), client.get('/'), client.get('/methods'),
167
+ ]);
168
+ return { health: health.data, root: root.data, methods: methods.data };
169
+ }
170
+ ```
171
+
172
+ Refresh button re-runs the parallel fetch. If any single call fails the others still render.
173
+
174
+ ### Dark mode
175
+
176
+ - `src/styles/tokens.css` defines `--bg`, `--surface`, `--text`, `--muted`, `--primary`, `--primary-strong`, `--motif-stroke`, `--motif-fill` for both light and dark, scoped under `:root` and `:root[data-theme="dark"]`.
177
+ - `src/stores/theme.js` (Pinia) initialises from `localStorage('theme') || matchMedia('(prefers-color-scheme: dark)') ? 'dark' : 'light'` and writes `document.documentElement.dataset.theme` on changes.
178
+ - `ThemeToggle.vue` is a Semantic `ui icon button` (sun ↔ moon).
179
+ - Light primary `#2e8b57` (sea green); dark primary `#3ddc97` (brighter for contrast on dark surfaces).
180
+
181
+ ### Responsive layout
182
+
183
+ - All pages wrapped in `ui container` (Semantic switches widths automatically per breakpoint).
184
+ - Homepage hero: stacked on mobile, side-by-side (avatar + text) on tablet+. Demo cards: `ui three column stackable cards` (collapses to single column on mobile).
185
+ - CV page: header section uses `ui stackable grid`. WorkExperience/Education items: logo column hidden on `mobile only`, stacked above on `tablet only`, side-by-side on `computer only`.
186
+ - Typography uses `clamp(1rem, 2.5vw, 1.25rem)` for body, similar scaling for headings.
187
+ - NavBar collapses to a Semantic `ui sidebar` triggered by a hamburger icon below 768 px.
188
+
189
+ ### CV content pipeline
190
+
191
+ - `src/frontend/src/data/cv.js` exports a structured object β€” single source of truth, transcribed from the PDF.
192
+ - Reusable components are dumb (props only). Optional `logo` prop on Work/Education items references `/logos/<slug>.svg`; missing file β†’ acronym badge fallback.
193
+ - Full merged Work Experience list (reverse-chronological):
194
+ 1. **Attercop** β€” Data Scientist, 08/2025–present, Remote / Brighton, UK.
195
+ 2. **EPFL** β€” Machine Learning Researcher (PhD), 11/2021–10/2025, Lausanne, Switzerland.
196
+ 3. **Data Masters** β€” Data Consultant, 03/2024–08/2024, Skopje, North Macedonia.
197
+ 4. **Swisscom** β€” ML & Data Analysis Intern, 02/2021–08/2021, Lausanne, Switzerland.
198
+ 5. **CERN** β€” Software Engineer intern (ISOLDE, Summer Student Programme), 06/2018–08/2018, Meyrin, Switzerland β€” PHP back-end for collider operator apps + TI dashboard; front-end migration from Angular to Vue.
199
+ 6. **Faculty of Computer Science and Engineering (FCSE/FINKI), Skopje** β€” Student Assistant, 02/2018–06/2019, Skopje, North Macedonia β€” part-time lab TA across 5 courses, 12 h/week for 3 semesters.
200
+ 7. **Endava** β€” Software Engineer intern, 07/2017–08/2017, Skopje, North Macedonia β€” 6-week training programme; built a three-part task-management app in AngularJS + Spring Boot + MySQL.
201
+ - Tagline: LinkedIn headline "Data Scientist & Researcher | Graph ML, Gen AI, Deep Learning" β€” used in `HeroIntro.vue` and the CV header.
202
+ - References section: **Andreas Loukas** (Isomorphic Labs), **Claudiu Musat** (Google DeepMind), **Volkan Cevher** (EPFL), plus **Martina Naumovska Necheska** (Data Masters β€” `martina.naumovska@datamasters.co`).
203
+ - Honors & Awards section (from LinkedIn, not in the short CV PDF): three scholarship/honorary-award items β€” worth rendering as a small list on `/cv`.
204
+ - Publications list on the website should add **"Scalable Methods for Knowledge Graph Reasoning and Generation"** (the PhD thesis itself, linked to `https://infoscience.epfl.ch/entities/publication/87acf391-feef-43a0-b665-7f2f0bc70b2c`).
205
+
206
+ ### Theming + favicon
207
+
208
+ - `index.html` favicon is an inline SVG data URL of the rabbit emoji: `<text font-size="80" y="80">πŸ‡</text>` wrapped in a 100Γ—100 SVG β€” no binary asset.
209
+ - Page title set per-route via `router.afterEach` writing `document.title`.
210
+
211
+ ### API client
212
+
213
+ - Single axios instance in `api/client.js` with `baseURL = ${VITE_API_BASE_URL}/api/v1`, 10 s timeout.
214
+ - Modules `coins.js` and `health.js` wrap individual calls. Errors expose `error.response?.data?.message` from the backend's error envelope.
215
+
216
+ ## Implementation Steps
217
+
218
+ 1. **Scaffold `src/frontend/`**: `npm create vite@latest frontend -- --template vue`; install `vue-router`, `pinia`, `axios`, `fomantic-ui-css`. Configure Vite, env files, npm scripts (`dev`, `build`, `preview`). Add rabbit-emoji favicon in `index.html`. Commit.
219
+ 2. **Shared layout + theming**: `App.vue`, `NavBar.vue` (Home/CV links + `ThemeToggle`), `tokens.css`, `theme.css`, `responsive.css`, Pinia `theme` store with persistence, mobile sidebar. Router with `/`, `/cv`, `/:pathMatch(.*)*` (404). Verify `npm run dev`.
220
+ 3. **Generate motif JSON**: `scripts/export_motifs.py`; run once; commit `src/frontend/src/data/motifs.json` (assert 13 + 199).
221
+ 4. **Extend sample-triples response with labels**: in `src/backend/api/views/coins.py`, call the existing `clean_entity_name` / `clean_relation_name` helpers from `api/utils.py` to add `subject_label`, `relation_label`, `object_label` alongside the raw IDs. Update `docs/api.yaml`, `src/backend/README.md`, and the Postman collection/environment under `docs/postman/` (add example response fields so the team can smoke-test the change).
222
+ 5. **Build `GraphBackground` + `FloatingMotif`**: SVG renderer, rAF drift, arrow markers, dark/light via CSS vars, mobile count reduction, reduced-motion fallback. Mount in `App.vue`.
223
+ 6. **API client**: `client.js`, `coins.js`, `health.js`. Smoke-test against local backend.
224
+ 7. **CV data + assets**: copy `docs/picture.jpg` β†’ `src/frontend/public/avatar.jpg`; transcribe `docs/Profile.pdf` verbatim into `src/frontend/src/data/cv.js` (Work Experience, Education, Skills, Languages, Projects, Publications, References, Honors & Awards, Profile summary).
225
+ 8. **Download logos**: EPFL, Swisscom, Attercop, **CERN**, **Endava**, UKIM/FINKI, Isomorphic Labs, Google DeepMind. Save as SVG where possible to `public/logos/`. Record source + licence in `public/logos/README.md`.
226
+ 9. **Reusable CV components**: `SocialLink`, `WorkExperience`, `Education`, `Skill`, `Language`, `Publication`, `Reference`, `ProjectCard`. Prop-driven; Semantic styling; logo-fallback acronym badge.
227
+ 10. **Homepage view**: `HomeView.vue` composing `HeroIntro` (avatar + bio from `cv.js`), education/work summary, three `DemoPreviewCard`s, `FactOfTheDay`, `SystemStatus`.
228
+ 11. **CV view**: `CVView.vue` composing components into sections (Header with photo + contact + socials; Profile; Experience; Education; Skills; Languages; Projects; Publications; References) inside `PageSection` wrappers.
229
+ 12. **Polish**: scroll-to-top on route change, page titles, error boundaries on widgets, mobile responsive QA at 320 / 375 / 768 / 1280 / 1920 px, dark-mode QA.
230
+ 13. **Manual verification** (see below).
231
+
232
+ ## Verification
233
+
234
+ Start backend (`python manage.py runserver` at `src/backend/`) and frontend (`npm run dev` at `src/frontend/`). In the browser:
235
+
236
+ - `/` renders: navbar (with theme toggle), animated graphs in the background, hero intro with the headshot, three demo teaser cards, **Fact of the Day** showing prettified NELL subject β†’ relation β†’ object pills (raw IDs visible on hover), and **System Status** card showing health dots for all three models, API version, and the methods list. All widgets reload via their refresh icons.
237
+ - `/cv` renders: full CV mirroring the PDF including CERN and Endava entries with logos; headshot in the header; every reusable component appears at least once.
238
+ - NavBar links route between `/` and `/cv` without a full reload; background animation persists across the route change. 404 route renders for unknown paths.
239
+ - Dark-mode toggle switches the entire UI (Semantic surfaces, motif background colors, text contrast). Refresh preserves the choice (`localStorage`). System dark-mode preference picked up on first load.
240
+ - Resize from 320 px β†’ 1920 px: layout reflows cleanly, no horizontal scroll, NavBar collapses to a hamburger sidebar below 768 px, CV cards stack on mobile.
241
+ - DevTools Network tab confirms `GET /api/v1/coins/datasets/nell/sample-triples?count=1`, `GET /api/v1/health`, `GET /api/v1/`, `GET /api/v1/methods` all 200.
242
+ - Toggle OS "reduce motion" β†’ motifs stop drifting but remain rendered.
243
+ - `npm run build && npm run preview` succeeds; rabbit-emoji favicon visible in the tab.
244
+ - Kill the backend β†’ both widgets show muted error states; rest of the homepage and CV still render.
245
+
246
+ ## Open Questions
247
+
248
+ (none β€” all employer/experience questions resolved via `docs/Profile.pdf`)