docs: establish approved Phase 1 design + Phase 2 spec as project foundation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Build output
|
||||
site/dist/
|
||||
site/.astro/
|
||||
**/bin/
|
||||
**/obj/
|
||||
|
||||
# Test artifacts
|
||||
tests/test-results/
|
||||
tests/playwright-report/
|
||||
tests/.cache/
|
||||
coverage/
|
||||
|
||||
# Env & secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.pfx
|
||||
relay/appsettings.*.local.json
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Mission prompt scratch files (kept locally, not part of the product)
|
||||
step1.txt
|
||||
step2.txt
|
||||
step3.txt
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
@@ -0,0 +1,50 @@
|
||||
# PROJECT_STATUS.md
|
||||
|
||||
Living status of the Phase 3 implementation. Updated continuously.
|
||||
|
||||
**Current phase:** Phase 3 — implementation
|
||||
**Branch strategy:** feature branches → PR → `main` (never commit directly to `main`)
|
||||
**Last updated:** 2026-07-03
|
||||
|
||||
---
|
||||
|
||||
## Milestones
|
||||
|
||||
| # | Milestone | Status |
|
||||
|---|---|---|
|
||||
| 0 | Repo, tooling, tracking (git, pnpm, docs moved to `docs/`) | 🟡 in progress |
|
||||
| 1 | Astro base config + token/style layer | ⬜ not started |
|
||||
| 2 | i18n layer (locales, slug map, dictionaries) | ⬜ not started |
|
||||
| 3 | Content collections + schemas + EN/NO data | ⬜ not started |
|
||||
| 4 | UI atoms + core layout (Header, Footer, SEO) | ⬜ not started |
|
||||
| 5 | Home page sections | ⬜ not started |
|
||||
| 6 | Case-study + capability templates | ⬜ not started |
|
||||
| 7 | Remaining pages (about, experience, contact, cv, colophon, 404) | ⬜ not started |
|
||||
| 8 | Behaviour modules (theme, nav, observer, lightbox, form) | ⬜ not started |
|
||||
| 9 | SEO layer (meta, hreflang, JSON-LD, OG, sitemap) | ⬜ not started |
|
||||
| 10 | Contact relay (.NET 9) | ⬜ not started |
|
||||
| 11 | Tests (vitest + playwright) | ⬜ not started |
|
||||
| 12 | Docker + CI | ⬜ not started |
|
||||
| 13 | Verification pass against acceptance criteria | ⬜ not started |
|
||||
|
||||
Legend: ⬜ not started · 🟡 in progress · ✅ done · ⛔ blocked
|
||||
|
||||
## Environment
|
||||
|
||||
- Node v22.23.1 · pnpm 9.15.9 · .NET SDK 10.0.201 (relay targets `net9.0`) · Docker 29.3.1
|
||||
- Repo initialised locally; remote `git.cesnimda.uk/cesnimda/ResumeSite.git` (push when instructed).
|
||||
|
||||
## Deviations from spec (with rationale)
|
||||
|
||||
_None yet. Any deviation gets logged here per the engineering rules._
|
||||
|
||||
## Open questions / blocked-on-user
|
||||
|
||||
_None yet._
|
||||
|
||||
## Prerequisites carried from Phase 1/2 (not blocking scaffold)
|
||||
|
||||
- Screenshot capture pass on seeded demo data (JobTrack, InboxIntel) — placeholders used until provided.
|
||||
- ATS-safe CV rework (EN/NO) — current PDFs wired as download targets meanwhile.
|
||||
- Native-Norwegian review pass of NO content — launch gate.
|
||||
- Legacy WordPress URL inventory for redirect/410 map.
|
||||
@@ -0,0 +1,59 @@
|
||||
# ResumeSite — Connor Babbington portfolio
|
||||
|
||||
Bilingual (English / Norwegian Bokmål) portfolio website. Static-first Astro site with a
|
||||
small self-hosted .NET contact relay, built to the specifications in [`docs/`](docs/).
|
||||
|
||||
- **Design system & UX** (Phase 1): [`docs/design/`](docs/design/README.md)
|
||||
- **Architecture & technical spec** (Phase 2): [`docs/spec/`](docs/spec/README.md)
|
||||
- **Build status**: [`PROJECT_STATUS.md`](PROJECT_STATUS.md) · **Work list**: [`TODO.md`](TODO.md)
|
||||
|
||||
## Stack
|
||||
|
||||
Astro 5 (static output) · TypeScript (strict) · Tailwind CSS 4 over CSS custom-property
|
||||
tokens · CSS-first motion (no animation library) · built-in i18n + typed dictionaries +
|
||||
content collections · zero client-state libraries · .NET 9 minimal-API contact relay ·
|
||||
pnpm / Vitest / Playwright · Docker + nginx, self-hosted.
|
||||
|
||||
## Quick start (local, without Docker)
|
||||
|
||||
```bash
|
||||
cd site
|
||||
pnpm install
|
||||
pnpm dev # http://localhost:4321
|
||||
```
|
||||
|
||||
## Quick start (Docker)
|
||||
|
||||
```bash
|
||||
# Development (HMR)
|
||||
docker compose -f deploy/docker-compose.dev.yml up
|
||||
|
||||
# Production build preview
|
||||
docker compose -f deploy/docker-compose.yml up --build
|
||||
```
|
||||
|
||||
See [`docs/spec/DOCKER_SPEC.md`](docs/spec/DOCKER_SPEC.md) and
|
||||
[`docs/spec/DEPLOYMENT_SPEC.md`](docs/spec/DEPLOYMENT_SPEC.md) for the full picture.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
site/ Astro project (pages, components, content, i18n, styles, scripts)
|
||||
relay/ .NET 9 minimal-API contact relay
|
||||
deploy/ compose files, nginx config, env templates
|
||||
tests/ Playwright end-to-end tests
|
||||
docs/ design (Phase 1) + spec (Phase 2)
|
||||
.gitea/ CI/CD workflows
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `pnpm dev` | Astro dev server with HMR |
|
||||
| `pnpm build` | Static production build → `site/dist/` |
|
||||
| `pnpm preview` | Serve the production build locally |
|
||||
| `pnpm check` | `astro check` + `tsc --noEmit` |
|
||||
| `pnpm lint` | ESLint + Prettier check |
|
||||
| `pnpm test` | Vitest unit tests |
|
||||
| `pnpm test:e2e` | Playwright end-to-end tests |
|
||||
@@ -0,0 +1,69 @@
|
||||
# TODO.md
|
||||
|
||||
Granular work list. Checked items are done; each maps to atomic commits.
|
||||
|
||||
## Milestone 0 — Scaffold
|
||||
- [x] Init git, enable pnpm, move docs into `docs/`
|
||||
- [x] Root `README.md`, `.gitignore`, `PROJECT_STATUS.md`, `TODO.md`
|
||||
- [ ] Initial commit on `main`, then feature branch
|
||||
- [ ] `site/` Astro project: `package.json`, `astro.config.mjs`, `tsconfig.json`, Tailwind config
|
||||
- [ ] ESLint + Prettier config
|
||||
- [ ] `pnpm install` + verify `pnpm build` on empty skeleton
|
||||
|
||||
## Milestone 1 — Tokens & styles
|
||||
- [ ] `styles/tokens.css` (colour/space/type/radius/motion, both themes via `[data-theme]`)
|
||||
- [ ] `styles/base.css` (reset, base element styles, focus ring, dot-grid, prose)
|
||||
- [ ] `styles/motion.css` (keyframes + reduced-motion layer)
|
||||
- [ ] Tailwind `@theme` wired to tokens; verify no raw hex/px in components (lint guard)
|
||||
|
||||
## Milestone 2 — i18n
|
||||
- [ ] `i18n/locales.ts` (locale config)
|
||||
- [ ] `i18n/slugMap.ts` (pageId ↔ {en,no} paths + section anchors) — the tested contract
|
||||
- [ ] `i18n/dictionary.en.ts` + `dictionary.no.ts` (typed, same interface)
|
||||
- [ ] `i18n/t.ts` accessor + `getLocale`/`resolvePath` helpers
|
||||
- [ ] Astro i18n config (defaultLocale en at root, no prefixed)
|
||||
|
||||
## Milestone 3 — Content
|
||||
- [ ] Zod schemas (`content.config.ts`) per DATA_MODEL
|
||||
- [ ] profile EN/NO · skills EN/NO · experience EN/NO · projects EN/NO (jobtrack, inboxintel, homelab) · meta pages
|
||||
- [ ] Locale-invariant field inheritance loader
|
||||
- [ ] Content parity + schema validation (feeds CI later)
|
||||
|
||||
## Milestone 4 — UI atoms + core
|
||||
- [ ] ui: Chip, Button, SplitCvButton, LangSwitch, ThemeToggle, Card, SectionLabel, TraceMotif, FramedImage, Tooltip
|
||||
- [ ] core: Base/Page/Home/CaseStudy layouts, Header, MobileNav, Footer, Seo, ThemeScript, SkipLink
|
||||
|
||||
## Milestone 5 — Home
|
||||
- [ ] Hero, ProofStrip, SkillsGrid, ProjectCards, ExperienceTimeline, AboutTeaser, ContactBand
|
||||
|
||||
## Milestone 6 — Case study
|
||||
- [ ] CsHeader, TldrBox, MiniToc, SectionRenderer, DecisionList, ArchDiagram, Gallery+Lightbox host, NextPrev
|
||||
- [ ] Capability template (homelab)
|
||||
|
||||
## Milestone 7 — Pages
|
||||
- [ ] Home (en/no), projects index, 3 case studies ×2, experience, about, contact, cv, colophon, 404
|
||||
|
||||
## Milestone 8 — Behaviour
|
||||
- [ ] theme.ts, nav.ts, observer.ts, lightbox.ts, form.ts (≤15 kB total)
|
||||
|
||||
## Milestone 9 — SEO
|
||||
- [ ] Seo component meta + hreflang from slug map
|
||||
- [ ] JSON-LD builders (Person, CreativeWork, WebSite, BreadcrumbList)
|
||||
- [ ] OG image generation, sitemap, robots.txt
|
||||
|
||||
## Milestone 10 — Relay
|
||||
- [ ] .NET 9 minimal API: validate, honeypot/time-trap, rate-limit, SMTP send, /healthz
|
||||
- [ ] appsettings + env config, Dockerfile
|
||||
|
||||
## Milestone 11 — Tests
|
||||
- [ ] vitest: dictionary parity, slug-map bijection, JSON-LD shape
|
||||
- [ ] playwright: both locales render, lang switch mapping, CV downloads, form contract, a11y (axe), keyboard
|
||||
|
||||
## Milestone 12 — Docker + CI
|
||||
- [ ] site Dockerfile (multi-stage → nginx) + nginx.conf (headers, caching, redirects)
|
||||
- [ ] relay Dockerfile
|
||||
- [ ] compose (prod + dev) + `.env.example`
|
||||
- [ ] `.gitea/workflows` CI pipeline (gates → build → images)
|
||||
|
||||
## Milestone 13 — Verification
|
||||
- [ ] Lighthouse budgets, axe clean, acceptance criteria walkthrough, PROJECT_STATUS final
|
||||
@@ -0,0 +1,91 @@
|
||||
# DESIGN_RESEARCH.md
|
||||
Portfolio Redesign — Phase 1 Research
|
||||
Connor Babbington · cesnimda.co.uk · July 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. Audit of the existing site
|
||||
|
||||
**Current state:** WordPress + Avada (Fusion Builder) single-page site. Sections: Hero → About → Proficiencies → Experience → Projects → CV downloads → Contact form.
|
||||
|
||||
### What works
|
||||
- The content skeleton is correct: summary, skills, experience, projects, CV, contact — everything a recruiter needs exists somewhere.
|
||||
- Clear availability statement ("open to remote, hybrid, on-site") in the first viewport.
|
||||
- Both CV languages are downloadable from the hero.
|
||||
|
||||
### What fails against the goal ("decide to interview me")
|
||||
| Problem | Evidence | Impact |
|
||||
|---|---|---|
|
||||
| **Generic template look** | Avada theme, stock section headings ("About.", "Let's Talk.") | A hiring manager evaluating a *software engineer* sees a page-builder site — it signals "assembled", not "engineered". The single strongest negative signal on the current site. |
|
||||
| **No proof, only claims** | Skills are bare word-lists; experience is one paragraph; projects have no screenshots, no links, no architecture, no outcomes | Recruiters can't verify; engineers can't assess depth. Nothing differentiates from 10,000 similar CVs. |
|
||||
| **Projects undersold** | Job Tracker gets 2 sentences; InboxIntel absent entirely | These two projects are the *strongest* seniority evidence available (multi-service architecture, OAuth, AI integration, Docker, testing, security hardening) and they're invisible. |
|
||||
| **English only** | `lang="en-GB"`, no Norwegian content | Candidate lives in Tønsberg targeting the Norwegian market; a Norwegian hiring manager gets zero accommodation. Bokmål content is also a trust and effort signal ("aktiv språkutvikling" made visible). |
|
||||
| **Heavy, slow stack** | WP + Avada + W3TC minified bundles, lazyload shims | Poor Core Web Vitals undercut the "production-level thinking" message. The site itself should be a work sample. |
|
||||
| **No dark mode, no motion design, no personality** | Static template sections | Feels dated next to any modern product site. |
|
||||
| **Weak SEO/meta** | Empty `og:title`, description is a raw text dump of the hero | Sloppy details on the most detail-oriented artifact a developer publishes. |
|
||||
|
||||
**Conclusion:** the redesign is not a reskin — the site must *become the work sample*: fast, precise, bilingual, with projects presented as engineering case studies.
|
||||
|
||||
---
|
||||
|
||||
## 2. What makes engineer portfolios convincing (pattern research)
|
||||
|
||||
Synthesised from high-performing engineer portfolios (e.g. the genre exemplified by Brittany Chiang, Lee Robinson, Josh Comeau, Rauno Freiberg) and product marketing sites (Stripe, Linear, Vercel, Notion, Framer):
|
||||
|
||||
### Convincing signals
|
||||
1. **Specificity beats adjectives.** "Reduced manual spreadsheet workflows for a county council" beats "passionate problem-solver". Numbers, stack names, and constraints read as experience.
|
||||
2. **Case studies, not thumbnails.** Portfolios that convert treat projects like product launches: problem → approach → architecture → decisions/trade-offs → outcome → screenshots. Engineers hiring engineers read the *trade-offs* section first.
|
||||
3. **The site itself is evidence.** Fast load, keyboard access, tasteful motion, working dark mode, correct typography — hiring managers notice these subconsciously; senior ones notice consciously.
|
||||
4. **Restraint reads as seniority.** Junior portfolios over-animate. The Linear/Stripe lesson: one or two signature moves, executed perfectly, on a quiet base.
|
||||
5. **Low-friction verification.** CV download, LinkedIn, e-mail, code links reachable within one interaction from anywhere on the page.
|
||||
6. **A human face.** A real photo near the top measurably increases trust and recall; recruiters match the site to the LinkedIn profile.
|
||||
|
||||
### What makes visitors leave
|
||||
- Walls of unscannable text (recruiters scan, engineers skim then deep-read).
|
||||
- "Skills: HTML, CSS, JavaScript…" laundry lists with no context or proficiency framing.
|
||||
- Broken/missing project links, placeholder content, lorem-tier copy.
|
||||
- Slow first paint; janky scroll-hijacking; autoplaying anything.
|
||||
- No way to answer "can this person do *my* job?" within ~30 seconds.
|
||||
|
||||
### SaaS landing-page techniques worth borrowing (not copying)
|
||||
| Site | Technique to adapt |
|
||||
|---|---|
|
||||
| **Stripe** | Dense-but-calm information design; gradient used as a single identity moment, not everywhere; impeccable code-adjacent typography. |
|
||||
| **Linear** | Dark-first palette; short declarative sentences; motion only on state change; keyboard-first affordances. |
|
||||
| **Vercel** | Monochrome + one accent; triangle-precision spacing; "proof strip" (logos/metrics) directly under the hero. |
|
||||
| **Notion** | Friendly humanity inside a rigid grid; illustration used sparingly to soften. |
|
||||
| **Framer** | Scroll-linked reveals that never block reading; hero that demonstrates the product (here: the site demonstrates the engineer). |
|
||||
|
||||
---
|
||||
|
||||
## 3. Recruiter & hiring-manager evidence base
|
||||
|
||||
(Expanded in RECRUITER_BEHAVIOUR_ANALYSIS.md.)
|
||||
|
||||
- **Time budget:** eye-tracking studies of recruiters (Ladders, 2018) put initial CV scan at **~7.4 seconds**. A portfolio gets slightly more — roughly 20–50 seconds before a stay/leave decision — but the first viewport carries the decision.
|
||||
- **Scan pattern:** F-pattern / layered scanning (Nielsen Norman Group): name → title → location/availability → most recent role → skills. Layout must place these on the F.
|
||||
- **What they screen for:** role fit keywords, seniority, location/work-permit, availability, red flags (gaps, vagueness). The hero must answer: *who, what level, what stack, where, available?*
|
||||
- **Two-audience problem:** recruiters (non-technical, 30s, checklist-driven) and engineering managers/CTOs (technical, 3–10 min, depth-driven) visit the same URL. The IA must serve a **fast lane** (scannable hero + skills + CV button) and a **deep lane** (case studies, architecture, decisions) without either blocking the other.
|
||||
- **Norway-specific:** Norwegian hiring culture values understatement, concrete competence claims (jf. *janteloven* — avoid grandiosity), and language effort. "Norsk A2/B1, i aktiv utvikling" stated plainly is a positive signal, not a weakness to hide. Work-permit clarity ("gyldig oppholdstillatelse") removes the #1 recruiter objection for foreign candidates.
|
||||
|
||||
---
|
||||
|
||||
## 4. Accessibility baseline (non-negotiable, also a seniority signal)
|
||||
|
||||
- WCAG 2.2 AA: contrast ≥ 4.5:1 body text (≥ 3:1 large text/UI), visible focus states, target size ≥ 24px.
|
||||
- Full keyboard operability including the language switch and theme toggle.
|
||||
- `prefers-reduced-motion` honoured by *every* animation (motion system must define reduced variants, not just "off").
|
||||
- Semantic landmark structure; skip-link; alt text strategy for project screenshots.
|
||||
- Language correctness: `lang` per locale, `hreflang` alternates — this doubles as SEO.
|
||||
|
||||
---
|
||||
|
||||
## 5. Strategic conclusions → design brief
|
||||
|
||||
1. **Reposition the site as a product.** Name the artefact: it is "Connor Babbington — systems developer" presented with the craft of a SaaS launch page.
|
||||
2. **Projects become the centre of gravity.** Two full case studies (JobTrack, InboxIntel) + an infrastructure/homelab capability page. Each with architecture diagram, screenshots, decisions, and honest "what I'd improve".
|
||||
3. **Two-lane IA.** 30-second lane: hero → proof strip → skills → CV. 10-minute lane: case studies → experience → about.
|
||||
4. **Bilingual as a feature, not a toggle afterthought.** Norwegian content is written (not machine-mirrored), the switch is visible in the header, and URLs are language-scoped.
|
||||
5. **Honest seniority framing.** The CV says "mid-level, 8 years". The site should not claim "senior" in a title; it should *demonstrate* senior judgement (trade-off writing, production thinking, security notes) and use the framing "systems developer, 8+ years". Evidence over adjectives — this is also the safest posture for the Norwegian market.
|
||||
6. **Performance is part of the design.** The visual system is deliberately light: system-adjacent type stacks, no hero video, SVG-first graphics, motion via transform/opacity only.
|
||||
7. **The design direction is defined in DESIGN_SYSTEM.md as "Kontrollrom / Quiet Console"** — a Nordic-restrained, dark-first engineering aesthetic with a single aurora-teal accent and monospace metadata as its identity move. Unique, ownable, and cheap to render.
|
||||
@@ -0,0 +1,62 @@
|
||||
# RECRUITER_BEHAVIOUR_ANALYSIS.md
|
||||
How recruiters and hiring managers actually behave on candidate sites — and the design responses.
|
||||
|
||||
---
|
||||
|
||||
## 1. The decision funnel
|
||||
|
||||
```
|
||||
Application / LinkedIn / referral
|
||||
│ (click, if the link looks worth it)
|
||||
▼
|
||||
First viewport ······· ~5–10 s → stay or bounce
|
||||
▼
|
||||
Scan pass ············ ~20–45 s → "worth shortlisting?"
|
||||
▼
|
||||
Depth pass (technical) ~3–10 min → "worth interviewing?"
|
||||
▼
|
||||
CV download / e-mail / forward to hiring manager
|
||||
```
|
||||
|
||||
Each stage has different owners: the scan pass is usually a recruiter; the depth pass an engineering manager. **The same page must pass both.**
|
||||
|
||||
## 2. Evidence & observed patterns
|
||||
|
||||
1. **~7.4 seconds** median initial CV scan (Ladders eye-tracking, 2018). Web pages get marginally more, but the fixation order is stable: **name → title → location → recent employer → dates → skills**. Design response: those six items are literally the hero and proof strip, in that order.
|
||||
2. **F-pattern scanning** (NN/g): two horizontal sweeps near the top, then a vertical skim down the left edge. Design response: left-align key nouns; front-load sentences; section labels in the left rail on desktop; never bury critical facts mid-paragraph or right-aligned.
|
||||
3. **Layer-cake scanning:** on well-structured pages, scanners read *headings only*. Design response: headings must carry the argument alone. Test: read only the H2/H3s — do they say "experienced .NET/full-stack developer in Norway with production evidence, available now"? Ship only if yes.
|
||||
4. **Checklist screening:** recruiters screen *out* before they screen in. Top objections for this candidate profile: (a) work permit / right to work in Norway, (b) language, (c) seniority mismatch, (d) unverifiable claims. Design response: kill all four above the fold — permit ✓, "English native / Norsk B1" ✓, "8+ yrs" ✓, links to real running systems ✓.
|
||||
5. **Mobile share is significant** for first visits from LinkedIn (often 40–60%). Design response: mobile hero carries identical decision-facts; CV download works on phone (and the CV itself is ATS-parseable — see CONTENT_STRATEGY §6).
|
||||
6. **Trust markers ranked** (approximate weighting for a mid/senior dev profile):
|
||||
- Real employer names + durations ●●●
|
||||
- Screenshots of real, running software ●●●
|
||||
- Face photo matching LinkedIn ●●
|
||||
- Code/architecture detail ●● (hiring managers)
|
||||
- Domain + professional e-mail ●
|
||||
- Testimonials ○ (low value unless verifiable — omit rather than fake)
|
||||
7. **Exit triggers:** template look, broken links, "under construction", walls of text, unlabeled skill bars ("JavaScript 80%" — meaningless, reads junior), autoplay, scroll hijack, pop-ups.
|
||||
|
||||
## 3. The two-lane rule (governing IA principle)
|
||||
|
||||
- **Fast lane (P1/P3):** hero → proof strip → skills matrix → CV/contact. Complete in ≤ 45 s without a single click. Every fact scannable, nothing interactive required.
|
||||
- **Deep lane (P2/P4):** project case studies and experience detail, one click away, each readable in 3–5 minutes with skimmable structure (headings carry the argument; diagrams before prose).
|
||||
- Lanes must not interleave: no case-study prose on the homepage beyond a 2-line teaser; no marketing fluff inside case studies.
|
||||
|
||||
## 4. Norway-market specifics
|
||||
|
||||
- **Understatement wins.** "Senior rockstar ninja" framing actively harms with Norwegian evaluators. Use concrete competence statements: "Åtte års erfaring med systemutvikling i britisk offentlig sektor."
|
||||
- **Public sector experience is an asset** — Warwickshire County Council maps mentally to kommune/fylkeskommune IT. Frame it that way in Norwegian copy.
|
||||
- **Language effort is a first-class signal.** A natively-written Bokmål site says more than "Norwegian: B1" ever can. Never machine-mirror; write shorter, plainer Norwegian rather than translated English sentence structures.
|
||||
- **Practicalities beat charisma:** permit, location (Tønsberg — commutable to Oslo south corridor, Horten, Sandefjord tech), remote readiness, start availability.
|
||||
|
||||
## 5. Measurable success criteria for the design (Phase 1 acceptance)
|
||||
|
||||
| # | Criterion | Test |
|
||||
|---|---|---|
|
||||
| 1 | Identity, role, stack, location, permit, availability all visible in first desktop viewport (1440×900) and first two mobile screens | Mockup review |
|
||||
| 2 | Headings alone tell the hire story | Read H-tags only |
|
||||
| 3 | CV (correct language) reachable in ≤ 1 interaction from any scroll position | Mockup review |
|
||||
| 4 | A technical reader can find an architecture diagram in ≤ 2 clicks | Journey walkthrough |
|
||||
| 5 | Language switch discoverable in ≤ 5 s, state obvious | Mockup review |
|
||||
| 6 | Nothing moves without user intent except one hero identity moment (≤ 800 ms, reduced-motion safe) | Motion spec |
|
||||
| 7 | All text ≥ 4.5:1 contrast in both themes | Colour system audit |
|
||||
@@ -0,0 +1,55 @@
|
||||
# USER_PERSONAS.md
|
||||
Four personas drive every layout and content decision. Each has a *time budget*, a *question they must answer*, and a *conversion action*.
|
||||
|
||||
---
|
||||
|
||||
## P1 — "Silje", Tech Recruiter (agency or in-house), Oslo/Vestfold
|
||||
- **Context:** Screening 40+ profiles today. Found the site via LinkedIn or an application link. Likely on desktop, possibly mobile between meetings. Norwegian native; reads English fine but a Norwegian option signals local commitment.
|
||||
- **Time budget:** 20–45 seconds.
|
||||
- **Technical depth:** Low. Matches keywords to a job spec.
|
||||
- **Must answer:** Right title? Right stack keywords (C#, .NET, React, Docker)? Located where? Work permit? Available? Language level? CV downloadable?
|
||||
- **Conversion action:** Download CV (correct language) or copy e-mail/LinkedIn into ATS.
|
||||
- **Design implications:** Hero states role, stack, location, permit and availability *in text* (not images). CV buttons persistent (hero + sticky nav + footer). Norwegian toggle prominent. Phone-friendly one-thumb layout.
|
||||
- **Failure mode:** Any ambiguity about location/permit → skip candidate.
|
||||
|
||||
## P2 — "Martin", Engineering Manager, product company (Oslo/remote-EU)
|
||||
- **Context:** Recruiter shortlisted Connor; Martin has 5 minutes between meetings to decide interview/no-interview. Desktop, probably dark-mode IDE person.
|
||||
- **Time budget:** 3–7 minutes.
|
||||
- **Technical depth:** High. Reads architecture, judges trade-offs, opens dev tools out of habit.
|
||||
- **Must answer:** Can he build and *operate* real systems? Does he test? Does he think about security and maintenance? Will he communicate well with stakeholders?
|
||||
- **Conversion action:** Skim one case study end-to-end → recommend interview; possibly click through to code.
|
||||
- **Design implications:** Case studies with architecture diagrams, decision/trade-off sections, testing & security notes, honest "known limitations". Site performance and a11y are silent evidence. Metadata in monospace, correct semantics — he *will* view source.
|
||||
- **Failure mode:** Claims without proof; a slow template site ("if the portfolio is Avada, what will the codebase be?").
|
||||
|
||||
## P3 — "Anne", CTO / Head of IT at a Norwegian SME or kommune-adjacent supplier
|
||||
- **Context:** Small team, hires rarely, values dependability over rockstar energy. Found via application. Reads Bokmål by preference.
|
||||
- **Time budget:** 2–5 minutes, often on the Norwegian version.
|
||||
- **Technical depth:** Medium-high, but evaluates *risk*: will this person maintain legacy systems, talk to users, stay?
|
||||
- **Must answer:** Public-sector experience? Boring-technology competence (SQL, .NET, servers)? Stability and communication? Settled in Norway?
|
||||
- **Conversion action:** Read About + Experience → forward to colleague → invite to interview.
|
||||
- **Design implications:** Warwickshire County Council experience framed as *public-sector systems work* (highly relevant in NO). Understated tone (janteloven-compatible). Norwegian copy natively written, not translated-sounding. "Bosatt i Tønsberg, gyldig oppholdstillatelse" visible.
|
||||
- **Failure mode:** Grandiose self-labels, hype language, English-only.
|
||||
|
||||
## P4 — "Priya", Senior Engineer doing interview-loop prep (secondary persona)
|
||||
- **Context:** Interview is booked; she's preparing questions from his projects. Deep-reads one case study.
|
||||
- **Time budget:** 10+ minutes.
|
||||
- **Must answer:** What exactly did he build? Where are the interesting decisions to probe?
|
||||
- **Conversion action:** Arrives at interview with informed, favourable questions.
|
||||
- **Design implications:** Case studies must be accurate and honest — everything on the page is fair game in an interview. "What I'd do differently" sections turn probing questions into prepared wins.
|
||||
|
||||
---
|
||||
|
||||
## Persona → priority matrix
|
||||
|
||||
| Section | P1 Recruiter | P2 Eng Manager | P3 CTO (NO) | P4 Engineer |
|
||||
|---|---|---|---|---|
|
||||
| Hero (role/stack/location/permit) | ●●● | ●● | ●●● | ● |
|
||||
| Skills matrix | ●●● | ●● | ●● | ● |
|
||||
| CV download | ●●● | ● | ●● | ● |
|
||||
| Case studies | ● | ●●● | ●● | ●●● |
|
||||
| Experience timeline | ●● | ●● | ●●● | ● |
|
||||
| About (human) | ● | ● | ●●● | ● |
|
||||
| Contact | ●●● | ●● | ●●● | ● |
|
||||
| Norwegian locale | ●● | ○ | ●●● | ○ |
|
||||
|
||||
**Reading:** The homepage top half serves P1/P3; the middle and project pages serve P2/P4. Nothing P2 needs may push P1's answers below the fold.
|
||||
@@ -0,0 +1,57 @@
|
||||
# CANONICAL_CONTENT.md
|
||||
Master source of truth for all site + CV copy (English). Provided by Connor, July 2026. All Phase 2/3 content derives from this file; the Norwegian versions are written natively against these *facts*, not translated sentence-by-sentence.
|
||||
|
||||
---
|
||||
|
||||
## Identity & contact
|
||||
- **Name:** Connor Babbington
|
||||
- **E-mail:** connor.babbington@cesnimda.co.uk
|
||||
- **Phone:** +47 41 33 44 70
|
||||
- **Website:** cesnimda.co.uk
|
||||
- **LinkedIn:** via redirect `cesnimda.co.uk/Linkedin`
|
||||
- **Location:** Tønsberg, Norway — valid residence permit
|
||||
- **Languages:** English (native) · Norwegian A2/B1, actively developing
|
||||
- **Availability:** open to remote, hybrid, or on-site developer roles
|
||||
|
||||
## Profile (canonical facts behind the About section)
|
||||
- Systems developer, eight years in UK local government (2015–2023).
|
||||
- Full-stack: backend, frontend, and server administration.
|
||||
- Analyses and plans user needs; programs and tests with modern languages/frameworks; designs robust, scalable systems.
|
||||
- Collaborates with developers, designers, project managers, and customers.
|
||||
- Strong experience: Azure DevOps (CI/CD), GitHub.
|
||||
- Works well independently and in teams; structured and open-ended tasks.
|
||||
|
||||
> Site framing decision (approved direction): self-label is **"Systems Developer"** — the CV's "mid-level" qualifier is dropped everywhere (site *and* reworked CV); seniority is shown through evidence. "Eight years" stays because the apprenticeship was on-the-job development work.
|
||||
|
||||
## Experience
|
||||
|
||||
### Warwickshire County Council, UK — System Developer · 2015–2023
|
||||
- **2015–2017 was an apprenticeship** → progressed to full developer role. Site treatment: show this *as progression* on the timeline (`2015–2017 apprentice → 2017–2023 developer`) — it's an asset (grew into the role, retained 8 years), not something to bury.
|
||||
- Developed and maintained multiple full-stack applications: **C#, Python, Ruby on Rails, SQL, JavaScript**.
|
||||
- Delivered reliable, well-tested software; resolved stability issues.
|
||||
- Translated stakeholder requirements into solutions; adapted across languages/frameworks as projects changed.
|
||||
- Responsibilities: backend + frontend design/build on several projects; bug fixing for stability and performance; clean maintainable code with testing and documentation; collaborative problem-solving; guided team members on code/process best practices.
|
||||
- **Concrete anchor (confirmed):** worked on the highways/streetlight fault-reporting system used across the whole of Warwickshire (county-wide public-facing + internal use). Draft site bullet: *"Worked on the county-wide highways and streetlight fault-reporting system used across Warwickshire."* Exact user counts unknown — say "county-wide", never invent numbers.
|
||||
|
||||
### 2023 → now (confirmed)
|
||||
- Self-directed development: JobTrack, InboxIntel, self-hosted infrastructure lab.
|
||||
- **Norskkurs** — ongoing Norwegian language study. Gets a timeline node (`2023– · Norskkurs + full-stack product development · Tønsberg`); it substantiates the "aktiv språkutvikling" claim and fills the employment gap honestly.
|
||||
|
||||
### Side roles (confirmed: part-time alongside the council role — vape shop weekend days, bar evenings/weekend evenings)
|
||||
- Royal Vapes, UK — Sales Representative · 2017–2021 (customer service, client issues, communications, payments)
|
||||
- The Hodcarrier, UK — Bartender · 2016–2018 (operations, multitasking)
|
||||
- Nuffield Health, UK — Receptionist · 2014–2015 (front of house, organisation)
|
||||
|
||||
Site treatment per CONTENT_STRATEGY: compact one-liners, de-emphasised, grouped under "Earlier & alongside: customer-facing roles that shaped how I communicate."
|
||||
|
||||
## Education
|
||||
- Warwickshire College, UK · 2012–2015 — **Extended Diploma NVQ Level 3 in ICT** (programming, systems administration, IT support).
|
||||
|
||||
## Interests
|
||||
PC and board games (strategic thinking, problem solving), cooking, learning new skills. Site treatment: one plain line in About; no icons, no over-explanation.
|
||||
|
||||
## Open items — all resolved July 2026
|
||||
1. ~~Side roles part-time?~~ Confirmed: weekend days (shop) and evenings/weekend evenings (bar), alongside the council role.
|
||||
2. ~~Concrete Warwickshire detail?~~ Confirmed: county-wide highways/streetlight fault-reporting system (see Experience above).
|
||||
3. ~~2023→now?~~ Confirmed: self-development + norskkurs (see section above).
|
||||
4. "Systems Developer, 8+ years" positioning: **approved by Connor.**
|
||||
@@ -0,0 +1,82 @@
|
||||
# CONTENT_STRATEGY.md
|
||||
|
||||
## 1. Voice & tone
|
||||
|
||||
| Attribute | Do | Don't |
|
||||
|---|---|---|
|
||||
| **Plain-spoken** | "I build and run web systems." | "Passionate technologist crafting digital experiences." |
|
||||
| **Evidence-led** | "Replaced spreadsheet-driven reporting workflows for a UK county council." | "Extensive experience with stakeholders." |
|
||||
| **Understated confidence** (Norwegian-market calibrated) | "Eight years building and maintaining production systems." | "Senior rockstar engineer." |
|
||||
| **Honest** | "SQLite was the right call for a single-user app; I'd move to Postgres for multi-tenant." | Hiding limitations. |
|
||||
|
||||
**Seniority framing decision:** the site never self-labels "senior". Role line = **"Systems Developer"** (EN) / **"Systemutvikler"** (NO), qualified by "8+ years". Seniority is demonstrated through trade-off writing, security notes, and production thinking. This is honest against the CV ("mid-level, eight years"), safe in Norwegian culture, and *more* convincing to P2/P3 than a claimed title.
|
||||
|
||||
## 2. Hero copy (approved-draft level)
|
||||
|
||||
**EN**
|
||||
> **Connor Babbington**
|
||||
> Systems Developer — .NET, full-stack & infrastructure
|
||||
> I design, build and run web systems: eight-plus years delivering internal software for UK local government, now building full-stack products in Norway.
|
||||
> Chips: `Tønsberg, Norway` `Work permit ✓` `Open to remote / hybrid / on-site` `English native · Norsk B1`
|
||||
> CTAs: **[View projects]** **[Download CV]**
|
||||
|
||||
**NO (written natively, not translated)**
|
||||
> **Connor Babbington**
|
||||
> Systemutvikler — .NET, fullstack og infrastruktur
|
||||
> Jeg utvikler og drifter websystemer. Åtte års erfaring med fagsystemer i britisk offentlig sektor — nå bygger jeg fullstack-løsninger fra Tønsberg.
|
||||
> Chips: `Bosatt i Tønsberg` `Gyldig oppholdstillatelse` `Åpen for remote / hybrid / oppmøte` `Engelsk morsmål · Norsk B1`
|
||||
> CTAs: **[Se prosjekter]** **[Last ned CV]**
|
||||
|
||||
## 3. Section content rules
|
||||
|
||||
- **Skills:** grouped chips + one context line per group. Never percentage bars, never star ratings. Groups: *Development* (C#, .NET, Python, JavaScript/TypeScript, React, SQL) · *DevOps & Infrastructure* (Docker, Linux, CI/CD, Azure DevOps, nginx/reverse proxies, monitoring, self-hosting) · *Practices* (testing, security hardening, OAuth2 integrations, stakeholder communication, production support). Each chip must be defensible in an interview.
|
||||
- **Experience:** the 2015–2023 Warwickshire role gets 3–4 highlight bullets (outcomes, not duties), with the 2015–2017 apprenticeship shown as *progression* on the timeline (apprentice → developer — a retention/growth signal, not a footnote). Non-dev roles (sales, bartender, receptionist — held alongside/before the council role) collapse into one line — "Earlier & alongside: customer-facing roles that shaped how I communicate" — present for timeline honesty, de-emphasised visually. NO version frames council work as "fagsystemer for lokalforvaltning". Canonical facts: see CANONICAL_CONTENT.md.
|
||||
- **About:** 3 short paragraphs — (1) how I work, (2) the homelab/personal-projects habit as evidence of currency, (3) life in Norway + language journey + interests (games → strategic thinking framing kept light). Photo: the outdoor headshot (warm, approachable); the suit photo reserved for CV/LinkedIn.
|
||||
|
||||
## 4. Case-study content sources & angles
|
||||
|
||||
### JobTrack (`/projects/jobtrack`)
|
||||
- **Source:** `D:\Job tracker\website_details.md` (already portfolio-shaped — reuse its headline, features, use cases) + README (stack: React, ASP.NET Core (.NET 9), SQLite, FastAPI summarizer, Ollama, Docker, PWA share-target).
|
||||
- **Angle:** *product thinking + integration depth.* Story: "I had a real problem (job hunting), built a real tool, then hardened it like production software."
|
||||
- **Decision candidates:** SQLite vs Postgres for single-user; local AI (Ollama) vs cloud API (privacy + cost); PWA share-target vs native app; no offline cache by design (deploy freshness) — this one is *gold*: a deliberate anti-feature with reasoning.
|
||||
- **Security notes:** Google ID-token auth, ownership checks, secure file uploads.
|
||||
|
||||
### InboxIntel (`/projects/inboxintel`)
|
||||
- **Source:** repo README + docs/ARCHITECTURE.md.
|
||||
- **Angle:** *architecture discipline.* Story: "Clean Architecture in practice: four-layer .NET solution, background sync worker, safe-by-design destructive operations."
|
||||
- **Decision candidates:** Clean Architecture layering & dependency rule; encrypted-at-rest OAuth refresh tokens (Data Protection API); preview-then-confirm for all destructive cleanup; advisory-only AI layer; Polly retry/backoff against Gmail API.
|
||||
- **Honesty note:** README calls it a scaffold with marked extension points — the case study says "in active development" with a roadmap, and status chip `In development`. Never overclaim; P4 will probe.
|
||||
|
||||
### Homelab (`/projects/homelab`) — capability page, lighter template
|
||||
- Ubuntu + Docker services, reverse proxy, auth, monitoring, self-hosted Gitea (git.cesnimda.uk — itself proof), backups. Angle: *operations competence* — "I don't just deploy; I keep things running." Include a small topology diagram.
|
||||
|
||||
### The site itself (footer meta-link)
|
||||
- One short page/footnote: performance budget, accessibility choices, bilingual architecture. Written *after* implementation (Phase 3 content); designed now as a P2 hook.
|
||||
|
||||
## 5. Localisation strategy (content level)
|
||||
|
||||
1. **Write EN and NO as siblings, not source→target.** NO sentences should be shorter and plainer; Bokmål tolerates directness English pads out.
|
||||
2. **Equivalence, not identity:** the NO About may spend more words on the Norway/language story; the EN version more on UK career detail. Facts identical; emphasis local.
|
||||
3. **Glossary (fixed term pairs):** Systems Developer/Systemutvikler · case study/prosjektgjennomgang · public sector/offentlig sektor · work permit/oppholdstillatelse · experience/erfaring · skills/kompetanse · self-hosted/egendriftet. Keep product names (JobTrack, InboxIntel) and technology names untranslated.
|
||||
4. **Quality gate:** a native/fluent Bokmål speaker reviews all NO copy before launch (flagged as an explicit pre-launch task — the current CV's Norwegian has minor tells, e.g. "Ytret fremragende kundeservice" should be "Ytet…"; the site must be cleaner than the CV).
|
||||
5. **Tone parity:** understated in both; the NO version must never read as marketing-translated.
|
||||
|
||||
## 6. CV improvement recommendations (assets provided → must improve)
|
||||
|
||||
The current PDFs are two-column, heavily letter-spaced graphical CVs. Problems: (a) letter-spaced headings ("E X P E R I E N C E") and two-column order break ATS parsing badly — text extraction confirms scrambled reading order; (b) content undersells projects; (c) "mid-level" self-label undercuts an 8-year record.
|
||||
|
||||
**Recommendations (content design, no implementation):**
|
||||
1. Produce a **single-column, ATS-safe layout**: standard headings (Experience, Education, Skills, Projects), no letter-spacing tricks, real text, consistent date formats.
|
||||
2. Replace "Mid-level system developer" with **"Systems developer with eight years' experience"** in both languages.
|
||||
3. Add a **Projects section** (JobTrack, InboxIntel, homelab — 2 lines each with stack) — currently the CV omits his strongest recent evidence entirely.
|
||||
4. Split the 2015–2023 role into outcome bullets mirroring site copy (single source of truth with the site content model).
|
||||
5. Fix Norwegian errors ("Ytret" → "Ytet", "holdt baren oppdatert på lager" → "holdt baren velfylt"); native review pass.
|
||||
6. Add the website URL prominently; site and CV cross-promote.
|
||||
7. Keep a designed "pretty" variant for humans if desired, but the *download default* is the ATS-safe version. Filenames: `Connor-Babbington-CV-EN.pdf` / `Connor-Babbington-CV-NO.pdf`.
|
||||
|
||||
## 7. SEO content (conceptual)
|
||||
|
||||
- Title patterns: `Connor Babbington — Systems Developer (.NET, React) · Tønsberg, Norway` / `Connor Babbington — Systemutvikler (.NET, React) · Tønsberg`. Case studies: `JobTrack — case study · Connor Babbington`.
|
||||
- Meta descriptions handwritten per page per language (≤ 155 chars), answering "who/what/where".
|
||||
- Structured data (conceptual): `Person` on home (name, jobTitle, address locality, sameAs → LinkedIn/Gitea), `SoftwareSourceCode`/`CreativeWork` per case study.
|
||||
- OpenGraph: per-language OG images (name + role line + accent motif; generated per design system).
|
||||
@@ -0,0 +1,91 @@
|
||||
# INFORMATION_ARCHITECTURE.md
|
||||
|
||||
## 1. Site map
|
||||
|
||||
```
|
||||
/ Home (EN default)
|
||||
/no Hjem (Bokmål)
|
||||
│
|
||||
├── /projects Projects index (teaser cards) /no/prosjekter
|
||||
│ ├── /projects/jobtrack Case study: JobTrack /no/prosjekter/jobtrack
|
||||
│ ├── /projects/inboxintel Case study: InboxIntel /no/prosjekter/inboxintel
|
||||
│ └── /projects/homelab Capability page: Self-hosted /no/prosjekter/hjemmelab
|
||||
│ infrastructure lab
|
||||
├── /experience Experience & timeline /no/erfaring
|
||||
├── /about About + photo + languages + interests /no/om-meg
|
||||
├── /contact Contact (form + direct channels) /no/kontakt
|
||||
└── /cv CV hub (EN/NO download, web-readable /no/cv
|
||||
version, ATS note)
|
||||
404 Styled, bilingual, links home
|
||||
```
|
||||
|
||||
**Depth rule:** nothing is more than 2 levels deep. Home teases everything; dedicated pages carry depth. This replaces the current one-pager because case studies need URLs of their own (shareable by recruiters into ATS/Slack, individually indexable per language).
|
||||
|
||||
**Home remains a narrative one-page scroll** (hero → proof → skills → featured projects → experience summary → about teaser → contact CTA) so the P1 fast lane never requires navigation. Deep pages serve P2/P4.
|
||||
|
||||
## 2. Navigation model
|
||||
|
||||
**Header (persistent, all pages):**
|
||||
```
|
||||
[CB monogram] Projects Experience About Contact | [EN/NO] [☾/☀] [Download CV ▾]
|
||||
```
|
||||
- Monogram → home. 4 nav items max — recruiters don't explore menus.
|
||||
- `Download CV ▾` is a split-button: primary action downloads the CV matching current locale; the chevron reveals the other language. Persistent = criterion "CV in ≤1 interaction from any scroll position".
|
||||
- Header is sticky, shrinks on scroll (see SCROLL_EXPERIENCE.md); backdrop blurs over content.
|
||||
- Mobile: monogram + CV button + hamburger (full-screen overlay panel, large tap targets, language + theme controls inside the panel top).
|
||||
|
||||
**Footer (all pages):** e-mail, LinkedIn, git.cesnimda.uk, CV both languages, language switch repeat, "Built by me — [how this site works]" link (small meta-page or case-study footnote; a deliberate P2 hook).
|
||||
|
||||
**Local navigation inside case studies:** right-side "On this page" mini-TOC (desktop ≥1200px only): Problem · Architecture · Decisions · Security · Outcome · Next.
|
||||
|
||||
## 3. Homepage section order & rationale
|
||||
|
||||
| # | Section | Serves | First-fact placement |
|
||||
|---|---|---|---|
|
||||
| 1 | **Hero** — name, role line, 2-line summary, location/permit/availability chips, CTAs (View projects / Download CV), portrait | P1 P3 | Everything from RECRUITER §2.1 |
|
||||
| 2 | **Proof strip** — 4 compact stat/fact tiles: `8+ yrs experience` · `UK public sector` · `.NET / React / Docker` · `Based in Tønsberg, NO` | P1 | Reinforces scan pass |
|
||||
| 3 | **Skills matrix** — 3 columns (Development / DevOps & Infrastructure / Practices), grouped chips with context lines, no percentage bars | P1 P2 | Keyword match |
|
||||
| 4 | **Featured projects** — 2 large case-study cards (JobTrack, InboxIntel) + 1 slim homelab card; each: screenshot, 1-line problem, stack chips, "Read case study →" | P2 P4 | Deep-lane entry |
|
||||
| 5 | **Experience** — compact vertical timeline (System Developer 2015–2023 emphasised; earlier roles collapsed one-liners) | P1 P3 | Employment verification |
|
||||
| 6 | **About teaser** — photo, 3 sentences, language levels, "More about me →" | P3 | Human trust |
|
||||
| 7 | **Contact CTA band** — "Looking for a systems developer in Norway?" + e-mail button + form link | all | Conversion |
|
||||
|
||||
## 4. Case study page template (both projects share it)
|
||||
|
||||
```
|
||||
1. Header: project mark + name, one-line value prop, status chip (Active / In development),
|
||||
stack chips, links (live demo if applicable · repository)
|
||||
2. Hero screenshot (framed browser mock, themed)
|
||||
3. TL;DR box — 4 bullets: What / Why / Stack / My role ← the only part P1 reads
|
||||
4. Problem & context
|
||||
5. Architecture — diagram first, then short prose (frontend / API / data / workers / integrations)
|
||||
6. Key decisions & trade-offs — 3–5 numbered decisions, each: choice, alternative, why
|
||||
7. Security & production notes — auth, secrets, encryption-at-rest, backups, CI
|
||||
8. Screenshots gallery — 3–5 annotated captures
|
||||
9. What I'd improve next — honest, specific
|
||||
10. Footer nav: ← other project | All projects
|
||||
```
|
||||
|
||||
Content per project sourced from `website_details.md` (JobTrack) and InboxIntel README/docs — see CONTENT_STRATEGY.md §4.
|
||||
|
||||
## 5. Bilingual architecture (conceptual)
|
||||
|
||||
- **URL strategy:** path-prefix locales. English at root (`/projects/jobtrack`), Norwegian under `/no/` with **localised slugs** (`/no/prosjekter/jobtrack`). Rationale: root-EN keeps existing inbound links and international reach; `/no/` prefix is the conventional, SEO-clean pattern; localised slugs signal genuine Norwegian content.
|
||||
- **Language switch behaviour:** switches to the *same page* in the other language (per-page mapping table), never to the other homepage. Preference remembered for return visits; first visit may show a one-time, dismissible "Denne siden finnes på norsk" hint if the browser prefers Norwegian — never an auto-redirect (recruiters share links across languages; links must stay stable).
|
||||
- **Parity rule:** every page exists in both languages; content is *equivalent, not identical* (CONTENT_STRATEGY §5). If a page ever ships EN-first, the NO version shows a short native-Norwegian summary + "full versjon på engelsk" link — never machine output, never a broken switch.
|
||||
- **SEO (conceptual):** `hreflang` pairs on every page + `x-default` → EN; per-language titles/descriptions/OpenGraph; per-language sitemap entries; canonical per locale (no cross-language canonicals — the versions are alternates, not duplicates).
|
||||
|
||||
## 6. Content model (design-level, informs Phase 2 data model)
|
||||
|
||||
```
|
||||
Profile name, roleLine ×2 lang, summary ×2, chips (location, permit,
|
||||
availability, languages), photo variants, links
|
||||
SkillGroup title ×2, ordered skills[] {name, contextLine ×2 (optional)}
|
||||
Project slug ×2, name, valueProp ×2, status, stack[], links[],
|
||||
tldr ×2, sections[] ×2 (problem/architecture/decisions/
|
||||
security/next), media[] {image, alt ×2, caption ×2}, diagram
|
||||
ExperienceItem employer, role ×2, period, location, summary ×2,
|
||||
highlights[] ×2, emphasis (featured | compact)
|
||||
Page meta title ×2, description ×2, ogImage per lang
|
||||
```
|
||||
Every user-visible string exists per-language by construction — this is the "avoid duplication issues" answer: one structural source, two content channels, no forked page trees.
|
||||
@@ -0,0 +1,52 @@
|
||||
# USER_JOURNEYS.md
|
||||
Five journeys validated against the IA. Format: step → what they see → design element that makes it succeed.
|
||||
|
||||
---
|
||||
|
||||
## J1 · Recruiter Silje — LinkedIn → CV in ATS (target: < 60 s)
|
||||
1. Clicks site link on LinkedIn (mobile). → Hero: "Connor Babbington — Systems Developer · .NET / React / Docker · Tønsberg, Norway · Open to work." Portrait matches LinkedIn photo. *(Identity confirmed in ~3 s.)*
|
||||
2. Thumb-scrolls once. → Proof strip chips: 8+ yrs · UK public sector · work permit ✓ · EN native/NO B1. *(All screening objections cleared without reading prose.)*
|
||||
3. Scrolls skills matrix. → Keyword match against job spec: C#, .NET, SQL, React, Docker, CI/CD. *(Chips, not bars — parseable at a glance.)*
|
||||
4. Taps persistent **Download CV** in header. → Gets `Connor-Babbington-CV-EN.pdf` (locale-matched). *(Split-button defaults correctly; no hunting.)*
|
||||
5. Done — candidate shortlisted. **Exit quality: converted without ever leaving the fast lane.**
|
||||
|
||||
## J2 · Eng manager Martin — shortlist e-mail → interview recommendation (target: ≤ 7 min)
|
||||
1. Opens link on desktop, dark IDE habits → site respects `prefers-color-scheme`, loads instantly. *(First silent evidence.)*
|
||||
2. Skims hero, ignores chips, clicks **Projects** in nav. → Two case-study cards with real screenshots + stack chips. Picks InboxIntel (Gmail/OAuth catches his eye).
|
||||
3. Case study: reads TL;DR box, jumps via mini-TOC to **Architecture**. → Clean-architecture diagram: React SPA → ASP.NET API → PostgreSQL, background sync worker, Gmail API, token encryption. *(Diagram before prose — his reading order.)*
|
||||
4. Reads **Decisions & trade-offs** ("why a hosted worker instead of a queue", "why advisory-only AI"). → Judgement demonstrated, not claimed.
|
||||
5. Reads **Security & production notes** (encrypted refresh tokens, confirmed-flag destructive ops, integration tests asserting authz). → *This* is the interview trigger for a production-minded manager.
|
||||
6. Opens dev tools out of habit → semantic HTML, no framework soup, fast. Checks the footer "how this site works" link, smirks approvingly.
|
||||
7. Replies to recruiter: "Yes, bring him in — ask about the sync worker." **Exit quality: P4 (interviewer prep) journey pre-seeded.**
|
||||
|
||||
## J3 · CTO Anne (Norwegian) — application → forwarded to colleague (target: ≤ 5 min)
|
||||
1. Opens link from application e-mail. Browser prefers `nb-NO` → dismissible hint: "Denne siden finnes på norsk →". Clicks it. → `/no` with natively-written Bokmål. *(Effort signal received before any content.)*
|
||||
2. Hero: "Systemutvikler · åtte års erfaring fra britisk offentlig sektor · bosatt i Tønsberg." *(Public-sector frame + local residence = risk questions answered.)*
|
||||
3. Reads **Erfaring**: Warwickshire County Council role described in kommune-relatable terms (saksbehandlingssystemer, rapportering, drift).
|
||||
4. Reads **Om meg**: photo, plain-spoken 3 paragraphs, "norsk B1, i aktiv utvikling", interests. *(Understated tone; janteloven-compatible.)*
|
||||
5. Copies URL of `/no/prosjekter/jobtrack` into e-mail to her senior dev. *(Per-language, per-page URLs make forwarding work.)*
|
||||
**Exit quality: converted in Norwegian end-to-end; never saw English.**
|
||||
|
||||
## J4 · Engineer Priya — interview prep deep-read (target: 10+ min, accuracy critical)
|
||||
1. Arrives directly on `/projects/jobtrack` (link from Martin). Reads whole case study top-to-bottom.
|
||||
2. Mini-TOC lets her jump back to Architecture while reading Decisions. Screenshots are annotated so she can reference specific UI ("the Gmail import view").
|
||||
3. **What I'd improve next** gives her interview questions the candidate is *prepared for*.
|
||||
**Design requirement surfaced: every claim in case studies must be true and demoable.**
|
||||
|
||||
## J5 · Return visitor / language edge cases
|
||||
- **Language switch mid-page:** Anne's colleague (English-speaking) receives the `/no/...` link → header `EN` switch → lands on `/projects/jobtrack`, same scroll context (same page mapping, position preserved where feasible).
|
||||
- **Theme:** system default on first visit; manual toggle persists; no flash of wrong theme on load (Phase 2 must solve this; Phase 1 flags it as a perceived-quality requirement).
|
||||
- **404:** mistyped shared link → bilingual 404 with links to both homepages and projects index. Never a dead end for a recruiter.
|
||||
- **Slow connection (mobile, train to Oslo):** text renders first; screenshots lazy-load below fold with fixed aspect-ratio placeholders (no layout shift — see MOTION_GUIDELINES loading states).
|
||||
|
||||
---
|
||||
|
||||
## Journey-derived requirements checklist
|
||||
- [x] Locale-matched CV default with one-tap access anywhere (J1)
|
||||
- [x] Architecture diagram ≤ 2 clicks from entry (J2)
|
||||
- [x] Security/testing content in every case study (J2)
|
||||
- [x] Native Bokmål, public-sector framing (J3)
|
||||
- [x] Stable per-language deep links; switch maps page↔page (J3, J5)
|
||||
- [x] Honest, demoable case-study claims (J4)
|
||||
- [x] No auto-redirect on language; hint pattern only (J3, J5)
|
||||
- [x] Zero layout shift on image load (J5)
|
||||
@@ -0,0 +1,58 @@
|
||||
# COLOUR_SYSTEM.md
|
||||
|
||||
## 1. Palette philosophy
|
||||
|
||||
Near-monochrome surfaces + **one** accent ("aurora" teal-green — a nod to Norway without cliché flags/fjords) + **one** functional secondary (amber, status-only). The accent behaves like a **status LED**: it marks liveness, focus, and action — it is never decoration or large fills.
|
||||
|
||||
## 2. Core tokens
|
||||
|
||||
### Dark theme (design lead)
|
||||
| Token | Value | Contrast vs surface-0 | Use |
|
||||
|---|---|---|---|
|
||||
| `surface-0` | `#0B0E14` | — | Page bg (blue-black, not pure black) |
|
||||
| `surface-1` | `#11151D` | — | Cards |
|
||||
| `surface-2` | `#171C26` | — | Nested panels, TL;DR |
|
||||
| `ink` | `#E8ECF2` | 15.4:1 | Headings, body |
|
||||
| `ink-muted` | `#9AA4B2` | 7.1:1 | Secondary text, captions |
|
||||
| `ink-faint` | `#5C6675` | 3.4:1 | Disabled, decorative only (never body) |
|
||||
| `border` | `#E8ECF2` @ 8% | — | Hairlines |
|
||||
| `accent` | `#3ECFAE` | 9.8:1 | Links, primary buttons (dark text on it), status dots, trace motif, focus rings |
|
||||
| `accent-ink` | `#062A22` | 12:1 on accent | Text on accent fills |
|
||||
| `amber` | `#E5A93D` | 8.3:1 | `IN DEVELOPMENT` chips, form warnings only |
|
||||
| `danger` | `#E5606B` | 5.6:1 | Form errors only |
|
||||
|
||||
### Light theme
|
||||
| Token | Value | Contrast vs surface-0 | Use |
|
||||
|---|---|---|---|
|
||||
| `surface-0` | `#FAFAF8` | — | Warm paper white |
|
||||
| `surface-1` | `#FFFFFF` | — | Cards |
|
||||
| `surface-2` | `#F1F2EF` | — | Nested panels |
|
||||
| `ink` | `#171A20` | 15.9:1 | Text |
|
||||
| `ink-muted` | `#555E6B` | 6.8:1 | Secondary |
|
||||
| `border` | `#171A20` @ 10% | — | Hairlines |
|
||||
| `accent` | `#0E8C72` | 4.9:1 | Links, buttons (white text: 5.1:1), focus |
|
||||
| `amber` | `#8A6114` | 5.6:1 | Status text (paired w/ `#F5E5C2` chip bg) |
|
||||
| `danger` | `#B3323E` | 6.2:1 | Errors |
|
||||
|
||||
**Note:** the accent is *perceptually the same identity* in both themes but shifted per theme to hold WCAG AA (dark uses the luminous `#3ECFAE`; light uses the deepened `#0E8C72`). This is deliberate — never reuse the dark accent on white (2.1:1, fails).
|
||||
|
||||
## 3. Usage rules
|
||||
|
||||
1. **Accent budget:** ≤ 3 accent moments per viewport (e.g. hero: trace line + primary CTA + one chip dot). If a screen feels flat, fix hierarchy with type/spacing — not more accent.
|
||||
2. **Large fills:** accent may fill only buttons and chips; never section backgrounds or headings-as-blocks.
|
||||
3. **Text on accent:** always `accent-ink`/white per table; never accent-on-accent.
|
||||
4. **Status semantics (fixed):** accent dot = active/live · amber dot = in development · muted dot = archived. Consistent across chips, timeline, project cards.
|
||||
5. **Charts/diagrams:** surfaces + border + ink-muted, accent for the *one* flow being explained, amber for external systems (e.g. Gmail API node). Max 4 hues in any diagram.
|
||||
6. **Duotone photo treatment (dark):** shadows → `#0B0E14`, highlights → warm neutral; accent never tints skin.
|
||||
7. **Never colour-only meaning:** every status pairs dot + text label; errors pair colour + icon + message.
|
||||
|
||||
## 4. Gradients
|
||||
|
||||
Exactly one permitted gradient: hero background halo — radial, accent @ 6% → transparent, anchored behind the trace motif. Everything else flat. (The Stripe lesson: one identity moment, not a gradient system.)
|
||||
|
||||
## 5. Accessibility audit summary
|
||||
|
||||
- All body/secondary text ≥ 4.5:1 in both themes (see tables).
|
||||
- UI components/borders on interactive elements ≥ 3:1 against adjacent colours.
|
||||
- Focus ring `accent` at 2px passes 3:1 against both surface-0 values.
|
||||
- `ink-faint` is documented decorative-only. High-contrast/forced-colors mode: dot-grid and halo disabled, hairlines → CanvasText.
|
||||
@@ -0,0 +1,80 @@
|
||||
# DESIGN_SYSTEM.md
|
||||
Design direction: **"Kontrollrom"** (control room) — a quiet, Nordic-restrained engineering aesthetic.
|
||||
|
||||
---
|
||||
|
||||
## 1. Concept
|
||||
|
||||
The site should feel like **well-run production infrastructure given a public face**: calm surfaces, precise alignment, information-dense but never busy, one living accent colour that behaves like a status light. Not a marketing site pretending to be technical; a technical artefact polished until it feels premium.
|
||||
|
||||
Three ideas fused, none copied:
|
||||
1. **Nordic restraint** — generous whitespace, muted surfaces, understatement (matches the Norwegian market and the candidate's voice).
|
||||
2. **Console/ops vernacular as *seasoning*** — monospace metadata labels, status chips, subtle grid/blueprint texture. Used at ~10% intensity: labels, captions, chips — never full terminal cosplay, no fake CLIs, no green-on-black.
|
||||
3. **SaaS product craft** — case studies presented like product launch pages (framed screenshots, TL;DR boxes, proof strips).
|
||||
|
||||
**Litmus test for every design decision:** *"Would this look at home in the admin console of a product you trust?"* If it's decorative noise — cut it.
|
||||
|
||||
## 2. Identity elements
|
||||
|
||||
- **Monogram:** `CB` set in the mono face inside a rounded square, with a 2px accent tick in the corner — reads as both initials and a status LED. Used as favicon, header mark, OG-image anchor.
|
||||
- **Signature motif — "the trace":** a thin (1.5px) accent line that draws horizontally then steps (like an oscilloscope/PCB trace). Appears once in the hero (animated draw on load), as section-divider punctuation, and in the 404. This is the site's single ownable visual move.
|
||||
- **Status chips:** rounded-full, mono, 12px, used for facts (location, permit, availability) and project status (`ACTIVE`, `IN DEVELOPMENT`). The chip *is* the brand's information atom.
|
||||
- **Photography:** the outdoor headshot, duotone-treated in dark mode (slight cool grade) and natural in light mode; always in a hairline-bordered rounded frame with a mono caption (`tønsberg, norway · 2025`).
|
||||
|
||||
## 3. Surfaces & elevation
|
||||
|
||||
Flat-first. Depth via **hairline borders (1px, low-alpha)** and background steps, not drop shadows.
|
||||
|
||||
| Token | Dark | Light | Use |
|
||||
|---|---|---|---|
|
||||
| `surface-0` | #0B0E14 | #FAFAF8 | Page background |
|
||||
| `surface-1` | #11151D | #FFFFFF | Cards, panels |
|
||||
| `surface-2` | #171C26 | #F1F2EF | Nested/hover panels, code-ish blocks |
|
||||
| `border` | #FFFFFF @ 8% | #16181D @ 10% | All hairlines |
|
||||
| `border-strong` | #FFFFFF @ 16% | #16181D @ 20% | Focused/hover cards |
|
||||
|
||||
Shadows: only two, both subtle, only in light mode + overlays (`shadow-card`, `shadow-overlay`). Dark mode uses border-strong + surface step instead.
|
||||
Radius scale: `4px` (chips inner) · `8px` (buttons, inputs) · `12px` (cards) · `16px` (screenshot frames) · `full` (chips).
|
||||
|
||||
## 4. Grid & layout
|
||||
|
||||
- Max content width **1200px**; case-study prose measure capped at **72ch**.
|
||||
- 12-column grid, 24px gutters desktop; 4-column, 16px gutters mobile.
|
||||
- Left-rail section labels on desktop (mono, vertical rhythm anchor): `01 — ABOUT`, `02 — SKILLS`… echoing the current site's numbering, done properly.
|
||||
- Breakpoints: 480 / 768 / 1024 / 1280 / 1536.
|
||||
- Background texture: an extremely faint dot-grid (border-colour dots, 24px pitch) on `surface-0` hero + section headers only — the "blueprint" whisper. Disabled in forced-colors/high-contrast.
|
||||
|
||||
## 5. Core components (design-level inventory)
|
||||
|
||||
| Component | States to design | Notes |
|
||||
|---|---|---|
|
||||
| Header / nav | default, scrolled (compressed + blur), mobile overlay | see mockups M7 |
|
||||
| Split CV button | default, hover, open (menu shows both languages + file size) | locale-aware default |
|
||||
| Language switch | EN active, NO active, hover, focus, keyboard | segmented control, see M11 |
|
||||
| Theme toggle | dark, light, hover (icon morph sun⇄moon) | respects system on first load |
|
||||
| Status chip | fact, status-active (accent dot pulse ≤ 2 loops), status-dev (amber dot) | |
|
||||
| Skill group card | default, hover (border-strong) | chips inside |
|
||||
| Project card | default, hover (screenshot parallax-shift 4px + border glow), focus | whole card clickable, single tab stop |
|
||||
| Timeline item | featured (expanded), compact (one-liner), hover | |
|
||||
| TL;DR box | — | surface-2, accent left edge (3px) |
|
||||
| Architecture diagram frame | static SVG, nodes highlight on hover (desktop only) | must read perfectly with zero interaction |
|
||||
| Screenshot frame | loading (skeleton), loaded, lightbox open | browser-chrome mock, aspect-locked |
|
||||
| Form field | default, focus, filled, error, success | error text + icon, never colour-only |
|
||||
| Buttons | primary (accent), secondary (outline), ghost | see MICRO_INTERACTIONS |
|
||||
| Footer | — | dense, mono-flavoured, sitemap + channels |
|
||||
| 404 | — | trace motif breaks/frays; bilingual links home |
|
||||
|
||||
## 6. Iconography & illustration
|
||||
|
||||
- Icons: single stroke-icon set, 1.5px stroke, 20px grid (Lucide-class aesthetic — final set is a Phase 2 choice). Never filled+outlined mixed.
|
||||
- No stock illustration, no 3D blobs, no particles. The only "illustrations" are: the trace motif, architecture diagrams (drawn in-system: surface-2 nodes, border hairlines, accent flow arrows, mono labels), and framed product screenshots.
|
||||
|
||||
## 7. Theming rules
|
||||
|
||||
- Dark is the **design-lead theme** (P2 audience default; screenshots of both apps present well on dark), light must be equally finished — P1/P3 recruiters often browse light.
|
||||
- Every token has a pair; no theme-conditional layouts. Accent identical in both (passes contrast both ways — see COLOUR_SYSTEM).
|
||||
- First paint must match user preference without flash (implementation constraint flagged for Phase 2).
|
||||
|
||||
## 8. What this system deliberately avoids
|
||||
|
||||
Percentage skill bars · testimonial carousels · particle heroes · scroll-hijacking · fake terminals · gradient-everything · cookie-consent theatre (no tracking → no banner; a privacy footnote instead — itself a trust signal).
|
||||
@@ -0,0 +1,50 @@
|
||||
# MOTION_GUIDELINES.md
|
||||
|
||||
## 1. Motion philosophy
|
||||
|
||||
**"Motion confirms; it never performs."** Every animation must answer a user question — *did my click register? what changed? where did this come from?* — or it doesn't ship. One exception: the hero trace draw, the site's single identity moment.
|
||||
|
||||
This is the strongest anti-junior signal available: restraint executed perfectly.
|
||||
|
||||
## 2. Tokens
|
||||
|
||||
| Token | Duration | Easing | Use |
|
||||
|---|---|---|---|
|
||||
| `instant` | 80ms | linear | Colour/opacity on hover |
|
||||
| `quick` | 160ms | ease-out (0.2, 0, 0, 1) | Buttons, chips, toggles, focus |
|
||||
| `standard` | 240ms | ease-out | Card hover lift, menu open, theme cross-fade |
|
||||
| `entrance` | 400ms | ease-out, slight overshoot-free | Section reveals (opacity + 12px rise) |
|
||||
| `signature` | 700ms | ease-in-out | Hero trace draw only, once per page load |
|
||||
|
||||
Rules: nothing exceeds 700ms; entrances stagger max 3 items × 60ms; **only `transform` and `opacity` animate** (performance budget — no layout/box-shadow/filter animation); parallax capped at 4px (project-card screenshots).
|
||||
|
||||
## 3. Reduced motion (first-class variant, not an off-switch)
|
||||
|
||||
`prefers-reduced-motion: reduce` →
|
||||
- Entrances become pure opacity fades (120ms), no translation.
|
||||
- Trace motif renders complete, static.
|
||||
- Theme switch: instant swap.
|
||||
- Status-dot pulse: static dot.
|
||||
- Scroll behaviour: instant anchor jumps, no smooth scroll.
|
||||
Nothing is *removed* — content parity is exact.
|
||||
|
||||
## 4. Loading states
|
||||
|
||||
| Context | Design |
|
||||
|---|---|
|
||||
| Initial page | No spinner, no splash: text renders immediately (fonts swap-safe), hero trace draws as the "loaded" confirmation. Perceived performance *is* the loading design. |
|
||||
| Screenshots / gallery | Aspect-ratio-locked skeleton: `surface-2` block + faint dot-grid + mono caption already visible (`loading capture…` / `laster skjermbilde…`); cross-fade 240ms on load. Zero layout shift, ever. |
|
||||
| Form submit | Button label → inline three-dot mono ellipsis animation (·, ··, ···), button stays same width; success swaps band to confirmation panel (see MICRO_INTERACTIONS §form). |
|
||||
| Language switch | Content cross-fade 160ms; header/footer chrome does not blink (only text nodes swap). |
|
||||
| Lightbox open | Backdrop fade 240ms + image scale 0.98→1. |
|
||||
|
||||
## 5. Page & theme transitions
|
||||
|
||||
- Route changes: outgoing fade 120ms → incoming rise-fade 240ms, header persistent. No full-screen wipes.
|
||||
- Theme toggle: 240ms token cross-fade; the toggle icon morphs (sun⇄moon path) in `quick`. Screenshots swap themed variants where available (JobTrack/InboxIntel both have dark UIs — capture both if the apps support it, else keep one honest capture).
|
||||
|
||||
## 6. Scroll-linked motion (summary — full spec in SCROLL_EXPERIENCE.md)
|
||||
|
||||
- Section reveals trigger at 20% viewport entry, once (no re-trigger on scroll-up).
|
||||
- Header compresses 72→56px between 0–120px scroll, linear-mapped.
|
||||
- No scroll-jacking, no pinned scenes, no horizontal hijack — recruiters skim; the scrollbar must behave like a document's.
|
||||
@@ -0,0 +1,28 @@
|
||||
# SPACING_SYSTEM.md
|
||||
|
||||
## 1. Scale (8pt base, 4pt half-step)
|
||||
|
||||
| Token | px | Use |
|
||||
|---|---|---|
|
||||
| `space-1` | 4 | Chip inner gaps, icon-to-label |
|
||||
| `space-2` | 8 | Inside chips/buttons vertical, tight stacks |
|
||||
| `space-3` | 12 | Button horizontal padding unit, list gaps |
|
||||
| `space-4` | 16 | Card inner padding (mobile), paragraph gaps |
|
||||
| `space-5` | 24 | Card inner padding (desktop), grid gutters |
|
||||
| `space-6` | 32 | Between related blocks |
|
||||
| `space-7` | 48 | Between component groups |
|
||||
| `space-8` | 64 | Section inner top/bottom (mobile) |
|
||||
| `space-9` | 96 | Section rhythm (desktop) |
|
||||
| `space-10` | 128 | Hero breathing room, page top/bottom |
|
||||
|
||||
Only these ten values exist. No 20px, no 30px, no eyeballing.
|
||||
|
||||
## 2. Rules
|
||||
|
||||
1. **Section rhythm:** homepage sections separated by `space-9` desktop / `space-8` mobile; the mono section label sits `space-6` above its H2.
|
||||
2. **Proximity encodes relationship:** gap within a skill group (`space-2`) < gap between groups (`space-5`) < gap between sections (`space-9`). If two things feel related but aren't, increase the gap before adding a divider; hairlines are the *second* resort.
|
||||
3. **Cards:** `space-5` padding desktop, `space-4` mobile; media flush to card edge (screenshots bleed to the frame), text content padded.
|
||||
4. **Touch targets:** min 44×44px tap area on mobile (chips visually smaller but hit-area padded); nav/footer link spacing ≥ `space-3` vertically on mobile.
|
||||
5. **Header:** 72px tall default, compresses to 56px on scroll; content top-padding accounts for it (anchor scroll offset = header height + `space-5`).
|
||||
6. **Prose:** paragraph gap `space-4`; heading top `space-7`/bottom `space-3` (the 2:1 before/after rule from TYPOGRAPHY_GUIDE).
|
||||
7. **Density gradient:** marketing-calm at top of pages → denser in technical sections (case-study decision lists, footer). Density shift is achieved by stepping down *one* spacing token, never by shrinking type below `small`.
|
||||
@@ -0,0 +1,43 @@
|
||||
# TYPOGRAPHY_GUIDE.md
|
||||
|
||||
## 1. Type roles (three voices, strict casting)
|
||||
|
||||
| Role | Direction | Character | Used for |
|
||||
|---|---|---|---|
|
||||
| **Display / headings** | Modern grotesque with slight technical edge — *Space Grotesk-class* (alternatives in that register: General Sans, Hanken Grotesk) | Confident, engineered, not cold | H1–H3, nav, buttons, stat numbers |
|
||||
| **Body** | Highly-legible humanist/neo-grotesque — *Inter-class* (alternatives: Source Sans 3, Figtree) | Invisible, effortless at 16–18px | Paragraphs, lists, form fields |
|
||||
| **Mono (identity voice)** | *JetBrains Mono-class* (alternatives: IBM Plex Mono, Commit Mono) | The "console seasoning" | Section labels, chips, captions, metadata, diagram labels, footer |
|
||||
|
||||
Exact typeface licensing/loading = Phase 2. Design intent: **two families max loaded + mono**, variable weights preferred, `ø æ å` coverage mandatory in all three (Norwegian body text is first-class, not fallback).
|
||||
|
||||
## 2. Scale (1.25 major-third, 16px base)
|
||||
|
||||
| Token | Size / line-height | Weight | Tracking | Use |
|
||||
|---|---|---|---|---|
|
||||
| `display` | 61/1.05 (desktop) · 40/1.1 (mobile) | 600 | −0.02em | Hero name only |
|
||||
| `h1` | 49/1.1 · 34/1.15 | 600 | −0.015em | Page titles |
|
||||
| `h2` | 39/1.15 · 28/1.2 | 600 | −0.01em | Section titles |
|
||||
| `h3` | 31/1.2 · 24/1.25 | 550 | 0 | Card titles, case-study subsections |
|
||||
| `h4` | 20/1.35 | 550 | 0 | Minor headings |
|
||||
| `body-lg` | 18/1.6 | 400 | 0 | Hero summary, TL;DR, lead paragraphs |
|
||||
| `body` | 16/1.65 | 400 | 0 | Default prose |
|
||||
| `small` | 14/1.5 | 400 | 0 | Secondary info, form hints |
|
||||
| `mono-label` | 12/1.4 | 500 | +0.08em, UPPERCASE | Section labels (`01 — PROSJEKTER`), chips |
|
||||
| `mono-meta` | 13/1.5 | 400 | 0 | Captions, dates, file sizes |
|
||||
|
||||
**Rule: uppercase + letter-spacing lives only in `mono-label`.** (The old site letter-spaced everything; the CV letter-spaces headings into A T S soup. Reserve the effect and it becomes identity instead of noise.)
|
||||
|
||||
## 3. Rhythm & measure
|
||||
|
||||
- Prose measure 60–72ch; hero summary ≤ 55ch.
|
||||
- Space *before* a heading ≈ 2× space after (sections cohere downward).
|
||||
- Baseline rhythm on the 8pt grid — line-heights above are chosen to land on it at 16px base.
|
||||
- Norwegian headline audit: NO strings run ~10–15% longer and compounds don't hyphenate freely (`systemadministrasjon`); every headline layout must be tested with the NO string at the mobile breakpoint. Wrapping rule: allow hyphenation in body NO text, never in headings — reword instead.
|
||||
|
||||
## 4. Typographic details (the seniority tells)
|
||||
|
||||
- Real dashes (– —), curly quotes per language convention (EN “ ” · NO « »), non-breaking spaces before units and inside `8+ yrs`.
|
||||
- Numbers in stats use tabular lining figures (mono or `tnum`).
|
||||
- Links in prose: accent-coloured underline (2px offset), full colour+underline — never colour-only (a11y).
|
||||
- Code/tech names inline (C#, .NET) stay in body face — mono inline is reserved for actual paths/commands in case studies.
|
||||
- Focus states use a 2px accent outline + 2px offset, same in both themes.
|
||||
@@ -0,0 +1,50 @@
|
||||
# ANIMATION_CONCEPTS.md
|
||||
Named animation concepts, each mapped to the question it answers. All obey MOTION_GUIDELINES tokens & reduced-motion variants.
|
||||
|
||||
---
|
||||
|
||||
## A1 · "Trace draw" — hero identity moment
|
||||
On first paint: a 1.5px accent line draws left→right under the role line (700ms `signature`), steps down 8px mid-way (PCB-trace corner), and terminates in a 4px status dot that blinks once, then stays lit next to `Open to work`.
|
||||
*Question answered:* "the page is ready — and he's available."
|
||||
Reduced motion: line + lit dot render complete, static. Repeat visits: draws once per session only.
|
||||
|
||||
## A2 · "Section arrive"
|
||||
Each homepage section reveals on 20% viewport entry: opacity 0→1 + 12px rise (400ms), heading first, content children staggered 60ms (max 3 groups). Fires once per page load.
|
||||
*Question:* "what's new in view?" — creates reading rhythm without withholding content (text is present for search/reader modes regardless).
|
||||
|
||||
## A3 · "Card focus"
|
||||
Project cards on hover/focus-visible: border → `border-strong`, screenshot shifts up 4px (240ms), title gains accent underline draw (160ms), `Read case study →` arrow nudges 4px right.
|
||||
*Question:* "is this clickable, and what happens?"
|
||||
|
||||
## A4 · "Chip pulse"
|
||||
The `ACTIVE` status dot pulses twice (scale 1→1.4 opacity fade) when its chip first enters viewport, then rests lit. Amber `IN DEVELOPMENT` dots never pulse (calm hierarchy: live > in-progress).
|
||||
*Question:* "which of these is running right now?"
|
||||
|
||||
## A5 · "Diagram flow" (case-study architecture, desktop only)
|
||||
Architecture diagrams are static SVGs; hovering a node highlights its connected edges in accent (160ms) and dims unrelated nodes to 60%. No autoplay, no looping data-packet animations.
|
||||
*Question:* "what talks to what?"
|
||||
Mobile/touch + reduced motion: full static diagram, always fully legible without interaction.
|
||||
|
||||
## A6 · "Theme morph"
|
||||
Sun⇄moon icon path-morph (160ms) + global 240ms token cross-fade. The hero trace re-tints without redrawing.
|
||||
|
||||
## A7 · "Language cross-fade"
|
||||
On EN⇄NO switch: text nodes fade out 80ms / in 160ms; layout containers keep dimensions during swap (pre-measured against longer NO strings) so nothing jumps. The switch control itself slides its active-segment thumb (160ms).
|
||||
*Question:* "same page, other language — nothing lost."
|
||||
|
||||
## A8 · "Timeline grow"
|
||||
Experience timeline's vertical hairline draws downward as the section enters (400ms); period dots fade in with their rows.
|
||||
*Question:* "read this top-down, newest first."
|
||||
|
||||
## A9 · "Form confirm"
|
||||
On successful submit, the form panel cross-fades to a confirmation card: accent check icon draws (240ms stroke), message + "I usually reply within a day" (`Jeg svarer vanligvis innen en dag`).
|
||||
*Question:* "did it actually send?" — the current site's broken-looking send feedback (raw � glyphs) is exactly what this replaces.
|
||||
|
||||
## A10 · "404 fray"
|
||||
The trace motif runs, stutters, and frays into dots at the break point; mono caption `route not found — no such page` / `fant ikke siden`. Static in reduced motion.
|
||||
*Purpose:* the one place allowed personality-forward motion — errors are where craft shows.
|
||||
|
||||
---
|
||||
|
||||
### Explicitly rejected concepts (documented so Phase 3 doesn't re-invent them)
|
||||
Typewriter hero text (junior tell) · particle backgrounds · 3D tilt cards · scroll-pinned storytelling · animated skill counters · magnetic buttons (see CURSOR_INTERACTIONS) · autoplaying screenshot carousels.
|
||||
@@ -0,0 +1,25 @@
|
||||
# CURSOR_INTERACTIONS.md
|
||||
|
||||
## Verdict: near-zero custom cursor work — by design
|
||||
|
||||
Custom cursors (dot followers, magnetic buttons, cursor-morphing) were evaluated and **rejected** for this site. Reasons, documented so the decision survives future redesign itch:
|
||||
|
||||
1. **Audience mismatch.** P1 recruiters on trackpads/mobile never see it; P2 engineering managers disproportionately *dislike* cursor gimmicks (input latency, hijacked expectations).
|
||||
2. **Brand mismatch.** "Kontrollrom" promises precision and restraint; a floating blob promises an agency reel.
|
||||
3. **Cost.** Pointer-following JS is the classic source of main-thread jank — the exact failure mode this site must never exhibit.
|
||||
|
||||
## What ships instead (native cursor semantics, used *correctly*)
|
||||
|
||||
| Context | Cursor | Note |
|
||||
|---|---|---|
|
||||
| Links, buttons, cards, TOC | `pointer` | Whole project card = pointer (it's one link) |
|
||||
| Fact chips (non-interactive) | `default` | Honest affordance — no fake clickability |
|
||||
| Text/prose | `text` | Never suppressed |
|
||||
| Screenshot in lightbox (zoomable) | `zoom-in` / `zoom-out` | The one "delight" allowed |
|
||||
| Click-to-copy e-mail | `copy` | Native, semantically perfect, zero JS cost |
|
||||
| Diagram nodes with hover-highlight | `default` + visual highlight | Hover is enhancement, not interaction — pointer would over-promise |
|
||||
| Disabled controls | `not-allowed` | |
|
||||
|
||||
## The single permitted flourish
|
||||
|
||||
On the hero, the status dot at the end of the trace shows a `crosshair`-style precision hover halo (pure CSS, 160ms) revealing a mono tooltip: `status: open to work` / `status: åpen for muligheter`. Desktop-only easter egg; invisible to touch, absent under reduced motion, no positional JS. If it ever measures as jank — cut it without debate.
|
||||
@@ -0,0 +1,54 @@
|
||||
# MICRO_INTERACTIONS.md
|
||||
Component-level interaction specs. Every interactive element defines: rest / hover / active / focus-visible / disabled, keyboard behaviour, and touch behaviour.
|
||||
|
||||
---
|
||||
|
||||
## Buttons
|
||||
- **Primary (accent fill):** hover → brightness +6% and 1px rise (transform); active → rise removed (press); focus-visible → 2px accent ring, 2px offset; disabled → surface-2 + ink-faint, no events.
|
||||
- **Secondary (outline):** hover → border-strong + surface-1 fill.
|
||||
- **Ghost (nav, footer):** hover → ink (from ink-muted) + accent underline draw.
|
||||
- Touch: no hover states persist; active state on touchstart (≤80ms feedback).
|
||||
|
||||
## Split CV button (header)
|
||||
- Primary zone downloads locale CV; label: `Download CV` / `Last ned CV`, mono-meta suffix shows size `· pdf 180 kB`.
|
||||
- Chevron zone (separate 44px hit area) opens a 2-item menu: `English (pdf)` / `Norsk (pdf)` with a subtle flag-free language tag (`EN`/`NO` mono chips — no flag icons anywhere; flags conflate language and nationality).
|
||||
- Keyboard: button reachable in tab order; chevron = separate stop; menu arrows + Esc. Download triggers a one-time toast-less confirmation: button icon swaps to check for 1.5s.
|
||||
|
||||
## Language switch (header, segmented control)
|
||||
- Two segments `EN | NO`, active segment has surface-1 thumb + ink text; inactive = ink-muted. Hover inactive → ink. Thumb slides 160ms on change (A7).
|
||||
- Keyboard: single tab stop, arrow keys toggle, Enter/Space activates. `aria` as a proper toggle group; screen reader announces "Norsk — bytt språk / English — switch language" (label in *target* language, the convention that works when you can't read the current one).
|
||||
- Mobile: inside menu overlay header, full-width segmented control, same behaviour.
|
||||
- Never a dropdown (two options), never flags, never auto-switching.
|
||||
|
||||
## Theme toggle
|
||||
- Icon-only button, morphing sun⇄moon; tooltip on hover-delay 600ms (`Switch to light mode` / `Bytt til lyst modus` — target-state phrasing).
|
||||
- First visit follows system; once touched, choice persists and system changes no longer override (standard expectation).
|
||||
|
||||
## Nav links
|
||||
- Rest: ink-muted → hover: ink + 2px accent underline drawing left→right (160ms). Current-page: ink + static accent underline + `aria-current`.
|
||||
- Scroll-spy on homepage: section currently in view marks its nav item (underline at 60% opacity) — subtle, no jumping.
|
||||
|
||||
## Chips
|
||||
- Fact chips: non-interactive, no hover (cursor default — honesty about affordance).
|
||||
- Stack chips on project cards: hover shows a 600ms-delay tooltip with one context line ("React 18 — SPA frontend"); tap on mobile: no-op (tooltip content lives in case-study text instead).
|
||||
|
||||
## Project cards
|
||||
- Whole card = one link (single tab stop); internal "Read case study →" is decorative affordance, not a second link. Hover per A3. Focus-visible: ring around entire card.
|
||||
- Screenshot inside frames: `alt` text written per language, meaningful ("JobTrack applications table with status filters", not "screenshot").
|
||||
|
||||
## Timeline items
|
||||
- Featured item expanded by default. Compact one-liners: entire row hover → surface-1 tint; they are *not* expandable (content lives in CV/experience page — avoid hidden-content traps for scanners).
|
||||
|
||||
## Form (contact)
|
||||
- Fields: 2px border; focus → accent border + ring; label always visible above (no placeholder-as-label). Error on blur-validate: danger border + icon + message under field (`Please enter a valid e-mail` / `Skriv inn en gyldig e-postadresse`); error summary focus-moved on failed submit.
|
||||
- Submit per A9. Honey-pot antispam invisible to AT (no CAPTCHA — friction kills recruiter conversion).
|
||||
- Beside the form, equal visual weight: direct e-mail (click-to-copy with `copied ✓ / kopiert ✓` mono feedback) and LinkedIn — many recruiters won't touch forms.
|
||||
|
||||
## Screenshot lightbox
|
||||
- Click/Enter opens; Esc, backdrop-click, or × closes; arrows navigate gallery; focus trapped; caption + `n/N` mono counter. Mobile: pinch-zoom enabled, swipe between captures.
|
||||
|
||||
## "On this page" mini-TOC (case studies, ≥1200px)
|
||||
- Items = ghost links with scroll-spy accent tick; click = smooth scroll (instant under reduced motion) with header-offset anchor.
|
||||
|
||||
## Copy affordances
|
||||
- E-mail and `git.cesnimda.uk` in footer/contact: click-to-copy with mono `copied ✓` swap (1.5s). Phone number: plain `tel:` link, no copy gimmick.
|
||||
@@ -0,0 +1,45 @@
|
||||
# SCROLL_EXPERIENCE.md
|
||||
|
||||
## 1. Governing rule
|
||||
|
||||
**The scrollbar is a contract.** Native scroll physics everywhere: no hijacking, no pinned scenes, no snap-scrolling on prose, no horizontal sections. Recruiters skim with momentum-flicks; anything that interferes reads as hostile.
|
||||
|
||||
## 2. Homepage scroll narrative (desktop)
|
||||
|
||||
| Scroll position | What happens | Why |
|
||||
|---|---|---|
|
||||
| 0 (load) | Hero static except trace draw (A1) | Decision facts readable instantly |
|
||||
| 0–120px | Header compresses 72→56px, gains blur backdrop + hairline bottom border (linear-mapped to scroll, not toggled — no pop) | Reclaims space, signals "we're moving" |
|
||||
| Each section at 20% entry | A2 section arrive, once | Reading rhythm |
|
||||
| Proof strip enter | Chip dots light in stagger (accent pulse A4) | Draws eye across the four facts |
|
||||
| Projects enter | Cards rise in 2-stagger; screenshots have 4px max parallax offset relative to card scroll | Depth without gimmick |
|
||||
| Experience enter | Timeline draw (A8) | Guides top-down reading |
|
||||
| Contact band enter | No animation — band is high-contrast already | Calm before conversion action |
|
||||
| Any position | Header CV button + nav always available | ≤1 interaction to CV rule |
|
||||
|
||||
Scroll-spy updates nav underline per section. No "back to top" button on homepage (header is sticky; document is ~5 viewports); case studies (longer) get one after 3 viewports, ghost-style, bottom-right.
|
||||
|
||||
## 3. Case-study scroll
|
||||
|
||||
- Mini-TOC (≥1200px) tracks reading position with an accent tick; no progress bars (this is a document, not an article funnel — but see next point).
|
||||
- Long-form sections use generous `space-9` rhythm; architecture diagram gets full-width breakout (max 1200px) beyond the 72ch prose measure.
|
||||
- Images lazy-load 1 viewport ahead with aspect-locked skeletons — **zero cumulative layout shift** is a hard requirement.
|
||||
- Section anchors offset by header height + 24px; anchor links in TL;DR jump correctly.
|
||||
|
||||
## 4. Mobile scroll
|
||||
|
||||
- No parallax, no scroll-linked header compression (fixed 56px slim header from the start); section reveals still fire (opacity-dominant).
|
||||
- Sticky elements budget: header only. No sticky CTAs covering content (thumb-reach conversion handled by the contact band + header CV icon-button).
|
||||
- Overscroll behaviour default; PWA-style rubber-banding untouched.
|
||||
- Gallery swipe is the only horizontal gesture, clearly contained inside the lightbox.
|
||||
|
||||
## 5. Deep-link & restoration behaviour
|
||||
|
||||
- Anchor navigation (nav click, TOC, TL;DR links): smooth scroll 400ms max — distance-capped so cross-page-length jumps don't take seconds; reduced motion → instant.
|
||||
- Back/forward restores scroll position (browser default — do not fight it).
|
||||
- Language switch preserves scroll ratio where section structure matches (A7); if the target page differs structurally, land at top of the equivalent section rather than a mismatched pixel offset.
|
||||
|
||||
## 6. Performance constraints on scroll effects
|
||||
|
||||
- All scroll-linked effects (header compression, spy, reveals) via passive observers/transforms — no scroll-handler layout reads (jank = instant credibility loss with P2).
|
||||
- Reveal thresholds via IntersectionObserver semantics (conceptually); everything degrades to "visible" when JS is absent — **content is never gated behind scroll animation.**
|
||||
@@ -0,0 +1,148 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 1420" font-family="Segoe UI, Arial, sans-serif">
|
||||
<defs>
|
||||
<radialGradient id="halo" cx="30%" cy="18%" r="55%">
|
||||
<stop offset="0%" stop-color="#3ECFAE" stop-opacity="0.07"/>
|
||||
<stop offset="100%" stop-color="#3ECFAE" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<pattern id="dots" width="24" height="24" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="1" fill="#E8ECF2" opacity="0.05"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- page -->
|
||||
<rect width="1440" height="1420" fill="#0B0E14"/>
|
||||
<rect width="1440" height="640" fill="url(#dots)"/>
|
||||
<rect width="1440" height="640" fill="url(#halo)"/>
|
||||
|
||||
<!-- ===== HEADER 72px ===== -->
|
||||
<line x1="0" y1="72" x2="1440" y2="72" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<!-- monogram -->
|
||||
<rect x="120" y="20" width="32" height="32" rx="8" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="136" y="41" fill="#E8ECF2" font-family="Consolas, monospace" font-size="13" font-weight="bold" text-anchor="middle">CB</text>
|
||||
<circle cx="148" cy="24" r="2" fill="#3ECFAE"/>
|
||||
<!-- nav -->
|
||||
<text x="420" y="41" fill="#9AA4B2" font-size="14">Projects</text>
|
||||
<text x="510" y="41" fill="#9AA4B2" font-size="14">Experience</text>
|
||||
<text x="620" y="41" fill="#9AA4B2" font-size="14">About</text>
|
||||
<text x="695" y="41" fill="#9AA4B2" font-size="14">Contact</text>
|
||||
<!-- language segmented -->
|
||||
<rect x="1000" y="22" width="88" height="28" rx="14" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="1003" y="25" width="41" height="22" rx="11" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="1023" y="40" fill="#E8ECF2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">EN</text>
|
||||
<text x="1066" y="40" fill="#9AA4B2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">NO</text>
|
||||
<!-- theme toggle -->
|
||||
<circle cx="1122" cy="36" r="14" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<path d="M1126 30 a7 7 0 1 0 3 9 a6 6 0 0 1 -3 -9" fill="#9AA4B2"/>
|
||||
<!-- CV split button -->
|
||||
<rect x="1156" y="20" width="164" height="32" rx="8" fill="#3ECFAE"/>
|
||||
<text x="1216" y="41" fill="#062A22" font-size="13" font-weight="600" text-anchor="middle">Download CV</text>
|
||||
<line x1="1290" y1="26" x2="1290" y2="46" stroke="#062A22" stroke-opacity="0.3"/>
|
||||
<path d="M1300 33 l6 6 l6 -6" fill="none" stroke="#062A22" stroke-width="1.8"/>
|
||||
|
||||
<!-- ===== HERO ===== -->
|
||||
<text x="120" y="196" fill="#3ECFAE" font-family="Consolas, monospace" font-size="12" letter-spacing="2">SYSTEMS DEVELOPER · TØNSBERG, NORWAY</text>
|
||||
<text x="120" y="262" fill="#E8ECF2" font-size="61" font-weight="600" letter-spacing="-1">Connor Babbington</text>
|
||||
<text x="120" y="316" fill="#E8ECF2" font-size="26" font-weight="500">I design, build and run web systems.</text>
|
||||
<text x="120" y="356" fill="#9AA4B2" font-size="18">Eight-plus years delivering internal software for UK local government —</text>
|
||||
<text x="120" y="384" fill="#9AA4B2" font-size="18">now building full-stack products in Norway.</text>
|
||||
|
||||
<!-- trace motif -->
|
||||
<path d="M120 424 H360 l12 12 H560" fill="none" stroke="#3ECFAE" stroke-width="1.5"/>
|
||||
<circle cx="566" cy="436" r="4" fill="#3ECFAE"/>
|
||||
<text x="582" y="441" fill="#9AA4B2" font-family="Consolas, monospace" font-size="12">status: open to work</text>
|
||||
|
||||
<!-- chips -->
|
||||
<g font-family="Consolas, monospace" font-size="12">
|
||||
<rect x="120" y="466" width="150" height="30" rx="15" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="195" y="485" fill="#9AA4B2" text-anchor="middle">Tønsberg, Norway</text>
|
||||
<rect x="282" y="466" width="128" height="30" rx="15" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="346" y="485" fill="#9AA4B2" text-anchor="middle">Work permit ✓</text>
|
||||
<rect x="422" y="466" width="200" height="30" rx="15" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="522" y="485" fill="#9AA4B2" text-anchor="middle">Remote / hybrid / on-site</text>
|
||||
<rect x="634" y="466" width="212" height="30" rx="15" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="740" y="485" fill="#9AA4B2" text-anchor="middle">English native · Norsk B1</text>
|
||||
</g>
|
||||
|
||||
<!-- CTAs -->
|
||||
<rect x="120" y="530" width="156" height="44" rx="8" fill="#3ECFAE"/>
|
||||
<text x="198" y="557" fill="#062A22" font-size="15" font-weight="600" text-anchor="middle">View projects</text>
|
||||
<rect x="292" y="530" width="196" height="44" rx="8" fill="none" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="390" y="557" fill="#E8ECF2" font-size="15" text-anchor="middle">Download CV · pdf</text>
|
||||
|
||||
<!-- portrait -->
|
||||
<rect x="1020" y="150" width="300" height="375" rx="16" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<circle cx="1170" cy="290" r="72" fill="#242B38"/>
|
||||
<path d="M1080 525 q90 -110 180 0" fill="#242B38"/>
|
||||
<text x="1170" y="300" fill="#5C6675" font-size="13" text-anchor="middle">[ outdoor headshot ]</text>
|
||||
<text x="1020" y="552" fill="#5C6675" font-family="Consolas, monospace" font-size="12">tønsberg, norway · 2025</text>
|
||||
|
||||
<!-- ===== PROOF STRIP ===== -->
|
||||
<g>
|
||||
<rect x="120" y="620" width="285" height="96" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="144" y="662" fill="#E8ECF2" font-size="28" font-weight="600">8+ yrs</text>
|
||||
<text x="144" y="692" fill="#5C6675" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">EXPERIENCE</text>
|
||||
<rect x="425" y="620" width="285" height="96" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="449" y="662" fill="#E8ECF2" font-size="22" font-weight="600">UK public sector</text>
|
||||
<text x="449" y="692" fill="#5C6675" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">BACKGROUND</text>
|
||||
<rect x="730" y="620" width="285" height="96" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="754" y="662" fill="#E8ECF2" font-size="22" font-weight="600">.NET · React · Docker</text>
|
||||
<text x="754" y="692" fill="#5C6675" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">CORE STACK</text>
|
||||
<rect x="1035" y="620" width="285" height="96" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="1059" y="662" fill="#E8ECF2" font-size="22" font-weight="600">Based in Norway</text>
|
||||
<text x="1059" y="692" fill="#5C6675" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">WORK PERMIT ✓</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== SKILLS SECTION ===== -->
|
||||
<text x="120" y="828" fill="#3ECFAE" font-family="Consolas, monospace" font-size="12" letter-spacing="2">01 — SKILLS</text>
|
||||
<text x="120" y="880" fill="#E8ECF2" font-size="39" font-weight="600">What I work with</text>
|
||||
<text x="120" y="916" fill="#9AA4B2" font-size="16">A practical profile: business value, maintainability, dependable delivery.</text>
|
||||
|
||||
<!-- three cards -->
|
||||
<g>
|
||||
<rect x="120" y="956" width="384" height="360" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="144" y="996" fill="#5C6675" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">01 — DEVELOPMENT</text>
|
||||
<text x="144" y="1032" fill="#9AA4B2" font-size="14">Backend-leaning full-stack work in the</text>
|
||||
<text x="144" y="1052" fill="#9AA4B2" font-size="14">.NET ecosystem.</text>
|
||||
<g font-family="Consolas, monospace" font-size="12" fill="#E8ECF2">
|
||||
<rect x="144" y="1080" width="52" height="28" rx="14" fill="#171C26"/><text x="170" y="1098" text-anchor="middle">C#</text>
|
||||
<rect x="204" y="1080" width="64" height="28" rx="14" fill="#171C26"/><text x="236" y="1098" text-anchor="middle">.NET</text>
|
||||
<rect x="276" y="1080" width="76" height="28" rx="14" fill="#171C26"/><text x="314" y="1098" text-anchor="middle">Python</text>
|
||||
<rect x="144" y="1118" width="110" height="28" rx="14" fill="#171C26"/><text x="199" y="1136" text-anchor="middle">TypeScript</text>
|
||||
<rect x="262" y="1118" width="66" height="28" rx="14" fill="#171C26"/><text x="295" y="1136" text-anchor="middle">React</text>
|
||||
<rect x="144" y="1156" width="56" height="28" rx="14" fill="#171C26"/><text x="172" y="1174" text-anchor="middle">SQL</text>
|
||||
<rect x="208" y="1156" width="110" height="28" rx="14" fill="#171C26"/><text x="263" y="1174" text-anchor="middle">JavaScript</text>
|
||||
</g>
|
||||
|
||||
<rect x="528" y="956" width="384" height="360" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="552" y="996" fill="#5C6675" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">02 — DEVOPS & INFRASTRUCTURE</text>
|
||||
<text x="552" y="1032" fill="#9AA4B2" font-size="14">I deploy and operate what I build —</text>
|
||||
<text x="552" y="1052" fill="#9AA4B2" font-size="14">professionally and at home.</text>
|
||||
<g font-family="Consolas, monospace" font-size="12" fill="#E8ECF2">
|
||||
<rect x="552" y="1080" width="76" height="28" rx="14" fill="#171C26"/><text x="590" y="1098" text-anchor="middle">Docker</text>
|
||||
<rect x="636" y="1080" width="66" height="28" rx="14" fill="#171C26"/><text x="669" y="1098" text-anchor="middle">Linux</text>
|
||||
<rect x="710" y="1080" width="66" height="28" rx="14" fill="#171C26"/><text x="743" y="1098" text-anchor="middle">CI/CD</text>
|
||||
<rect x="552" y="1118" width="130" height="28" rx="14" fill="#171C26"/><text x="617" y="1136" text-anchor="middle">Azure DevOps</text>
|
||||
<rect x="690" y="1118" width="150" height="28" rx="14" fill="#171C26"/><text x="765" y="1136" text-anchor="middle">Reverse proxies</text>
|
||||
<rect x="552" y="1156" width="120" height="28" rx="14" fill="#171C26"/><text x="612" y="1174" text-anchor="middle">Monitoring</text>
|
||||
<rect x="680" y="1156" width="130" height="28" rx="14" fill="#171C26"/><text x="745" y="1174" text-anchor="middle">Self-hosting</text>
|
||||
</g>
|
||||
|
||||
<rect x="936" y="956" width="384" height="360" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="960" y="996" fill="#5C6675" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">03 — PRACTICES</text>
|
||||
<text x="960" y="1032" fill="#9AA4B2" font-size="14">Production thinking: testing, security,</text>
|
||||
<text x="960" y="1052" fill="#9AA4B2" font-size="14">and honest communication.</text>
|
||||
<g font-family="Consolas, monospace" font-size="12" fill="#E8ECF2">
|
||||
<rect x="960" y="1080" width="86" height="28" rx="14" fill="#171C26"/><text x="1003" y="1098" text-anchor="middle">Testing</text>
|
||||
<rect x="1054" y="1080" width="130" height="28" rx="14" fill="#171C26"/><text x="1119" y="1098" text-anchor="middle">OAuth2 flows</text>
|
||||
<rect x="960" y="1118" width="180" height="28" rx="14" fill="#171C26"/><text x="1050" y="1136" text-anchor="middle">Security hardening</text>
|
||||
<rect x="1148" y="1118" width="150" height="28" rx="14" fill="#171C26"/><text x="1223" y="1136" text-anchor="middle">Prod. support</text>
|
||||
<rect x="960" y="1156" width="220" height="28" rx="14" fill="#171C26"/><text x="1070" y="1174" text-anchor="middle">Stakeholder comms</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- annotations -->
|
||||
<g font-family="Consolas, monospace" font-size="11" fill="#E5A93D">
|
||||
<text x="120" y="106">A1: trace draws on load (700ms), dot blinks once → stays lit</text>
|
||||
<text x="920" y="106">hover: nav underline draw · CV button = locale default + chevron menu</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,94 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 760" font-family="Segoe UI, Arial, sans-serif">
|
||||
<defs>
|
||||
<radialGradient id="haloL" cx="30%" cy="18%" r="55%">
|
||||
<stop offset="0%" stop-color="#0E8C72" stop-opacity="0.06"/>
|
||||
<stop offset="100%" stop-color="#0E8C72" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<pattern id="dotsL" width="24" height="24" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="1" fill="#171A20" opacity="0.05"/>
|
||||
</pattern>
|
||||
<filter id="cardShadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#171A20" flood-opacity="0.06"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect width="1440" height="760" fill="#FAFAF8"/>
|
||||
<rect width="1440" height="640" fill="url(#dotsL)"/>
|
||||
<rect width="1440" height="640" fill="url(#haloL)"/>
|
||||
|
||||
<!-- HEADER -->
|
||||
<line x1="0" y1="72" x2="1440" y2="72" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<rect x="120" y="20" width="32" height="32" rx="8" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.2"/>
|
||||
<text x="136" y="41" fill="#171A20" font-family="Consolas, monospace" font-size="13" font-weight="bold" text-anchor="middle">CB</text>
|
||||
<circle cx="148" cy="24" r="2" fill="#0E8C72"/>
|
||||
<text x="420" y="41" fill="#555E6B" font-size="14">Projects</text>
|
||||
<text x="510" y="41" fill="#555E6B" font-size="14">Experience</text>
|
||||
<text x="620" y="41" fill="#555E6B" font-size="14">About</text>
|
||||
<text x="695" y="41" fill="#555E6B" font-size="14">Contact</text>
|
||||
<rect x="1000" y="22" width="88" height="28" rx="14" fill="#F1F2EF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<rect x="1003" y="25" width="41" height="22" rx="11" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.2"/>
|
||||
<text x="1023" y="40" fill="#171A20" font-family="Consolas, monospace" font-size="11" text-anchor="middle">EN</text>
|
||||
<text x="1066" y="40" fill="#555E6B" font-family="Consolas, monospace" font-size="11" text-anchor="middle">NO</text>
|
||||
<circle cx="1122" cy="36" r="14" fill="#F1F2EF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<circle cx="1122" cy="36" r="5" fill="#555E6B"/>
|
||||
<g stroke="#555E6B" stroke-width="1.4">
|
||||
<line x1="1122" y1="26" x2="1122" y2="29"/><line x1="1122" y1="43" x2="1122" y2="46"/>
|
||||
<line x1="1112" y1="36" x2="1115" y2="36"/><line x1="1129" y1="36" x2="1132" y2="36"/>
|
||||
</g>
|
||||
<rect x="1156" y="20" width="164" height="32" rx="8" fill="#0E8C72"/>
|
||||
<text x="1216" y="41" fill="#FFFFFF" font-size="13" font-weight="600" text-anchor="middle">Download CV</text>
|
||||
<line x1="1290" y1="26" x2="1290" y2="46" stroke="#FFFFFF" stroke-opacity="0.4"/>
|
||||
<path d="M1300 33 l6 6 l6 -6" fill="none" stroke="#FFFFFF" stroke-width="1.8"/>
|
||||
|
||||
<!-- HERO (Norwegian locale shown — demonstrates NO string lengths + light theme) -->
|
||||
<text x="120" y="196" fill="#0E8C72" font-family="Consolas, monospace" font-size="12" letter-spacing="2">SYSTEMUTVIKLER · TØNSBERG</text>
|
||||
<text x="120" y="262" fill="#171A20" font-size="61" font-weight="600" letter-spacing="-1">Connor Babbington</text>
|
||||
<text x="120" y="316" fill="#171A20" font-size="26" font-weight="500">Jeg utvikler og drifter websystemer.</text>
|
||||
<text x="120" y="356" fill="#555E6B" font-size="18">Åtte års erfaring med fagsystemer i britisk offentlig sektor —</text>
|
||||
<text x="120" y="384" fill="#555E6B" font-size="18">nå bygger jeg fullstack-løsninger fra Tønsberg.</text>
|
||||
|
||||
<path d="M120 424 H360 l12 12 H560" fill="none" stroke="#0E8C72" stroke-width="1.5"/>
|
||||
<circle cx="566" cy="436" r="4" fill="#0E8C72"/>
|
||||
<text x="582" y="441" fill="#555E6B" font-family="Consolas, monospace" font-size="12">status: åpen for muligheter</text>
|
||||
|
||||
<g font-family="Consolas, monospace" font-size="12">
|
||||
<rect x="120" y="466" width="158" height="30" rx="15" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<text x="199" y="485" fill="#555E6B" text-anchor="middle">Bosatt i Tønsberg</text>
|
||||
<rect x="290" y="466" width="200" height="30" rx="15" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<text x="390" y="485" fill="#555E6B" text-anchor="middle">Gyldig oppholdstillatelse</text>
|
||||
<rect x="502" y="466" width="230" height="30" rx="15" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<text x="617" y="485" fill="#555E6B" text-anchor="middle">Remote / hybrid / oppmøte</text>
|
||||
<rect x="744" y="466" width="230" height="30" rx="15" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<text x="859" y="485" fill="#555E6B" text-anchor="middle">Engelsk morsmål · Norsk B1</text>
|
||||
</g>
|
||||
|
||||
<rect x="120" y="530" width="156" height="44" rx="8" fill="#0E8C72"/>
|
||||
<text x="198" y="557" fill="#FFFFFF" font-size="15" font-weight="600" text-anchor="middle">Se prosjekter</text>
|
||||
<rect x="292" y="530" width="196" height="44" rx="8" fill="none" stroke="#171A20" stroke-opacity="0.2"/>
|
||||
<text x="390" y="557" fill="#171A20" font-size="15" text-anchor="middle">Last ned CV · pdf</text>
|
||||
|
||||
<!-- portrait -->
|
||||
<rect x="1020" y="150" width="300" height="375" rx="16" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.1" filter="url(#cardShadow)"/>
|
||||
<circle cx="1170" cy="290" r="72" fill="#E4E6E1"/>
|
||||
<path d="M1080 525 q90 -110 180 0" fill="#E4E6E1"/>
|
||||
<text x="1170" y="300" fill="#8A93A0" font-size="13" text-anchor="middle">[ outdoor headshot — natural grade ]</text>
|
||||
<text x="1020" y="552" fill="#8A93A0" font-family="Consolas, monospace" font-size="12">tønsberg, norge · 2025</text>
|
||||
|
||||
<!-- proof strip -->
|
||||
<g filter="url(#cardShadow)">
|
||||
<rect x="120" y="620" width="285" height="96" rx="12" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<rect x="425" y="620" width="285" height="96" rx="12" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<rect x="730" y="620" width="285" height="96" rx="12" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
<rect x="1035" y="620" width="285" height="96" rx="12" fill="#FFFFFF" stroke="#171A20" stroke-opacity="0.1"/>
|
||||
</g>
|
||||
<text x="144" y="662" fill="#171A20" font-size="28" font-weight="600">8+ år</text>
|
||||
<text x="144" y="692" fill="#8A93A0" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">ERFARING</text>
|
||||
<text x="449" y="662" fill="#171A20" font-size="22" font-weight="600">Offentlig sektor (UK)</text>
|
||||
<text x="449" y="692" fill="#8A93A0" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">BAKGRUNN</text>
|
||||
<text x="754" y="662" fill="#171A20" font-size="22" font-weight="600">.NET · React · Docker</text>
|
||||
<text x="754" y="692" fill="#8A93A0" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">KJERNESTACK</text>
|
||||
<text x="1059" y="662" fill="#171A20" font-size="22" font-weight="600">Bosatt i Norge</text>
|
||||
<text x="1059" y="692" fill="#8A93A0" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">OPPHOLDSTILLATELSE ✓</text>
|
||||
|
||||
<text x="120" y="106" fill="#8A6114" font-family="Consolas, monospace" font-size="11">M2: light theme + Norwegian locale — same geometry as M1, token swap only; accent deepened to #0E8C72 for AA contrast</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,125 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 1720" font-family="Segoe UI, Arial, sans-serif">
|
||||
<rect width="1440" height="1720" fill="#0B0E14"/>
|
||||
|
||||
<!-- header (compressed 56px scrolled state) -->
|
||||
<rect width="1440" height="56" fill="#0B0E14" fill-opacity="0.9"/>
|
||||
<line x1="0" y1="56" x2="1440" y2="56" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="120" y="14" width="28" height="28" rx="7" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="134" y="33" fill="#E8ECF2" font-family="Consolas, monospace" font-size="12" font-weight="bold" text-anchor="middle">CB</text>
|
||||
<text x="430" y="34" fill="#E8ECF2" font-size="13">Projects</text>
|
||||
<line x1="430" y1="40" x2="478" y2="40" stroke="#3ECFAE" stroke-width="2"/>
|
||||
<text x="510" y="34" fill="#9AA4B2" font-size="13">Experience</text>
|
||||
<text x="610" y="34" fill="#9AA4B2" font-size="13">About</text>
|
||||
<text x="680" y="34" fill="#9AA4B2" font-size="13">Contact</text>
|
||||
<rect x="1176" y="13" width="144" height="30" rx="8" fill="#3ECFAE"/>
|
||||
<text x="1238" y="33" fill="#062A22" font-size="12" font-weight="600" text-anchor="middle">Download CV ▾</text>
|
||||
|
||||
<!-- case study header -->
|
||||
<text x="120" y="140" fill="#3ECFAE" font-family="Consolas, monospace" font-size="12" letter-spacing="2">CASE STUDY · PERSONAL PRODUCT</text>
|
||||
<text x="120" y="204" fill="#E8ECF2" font-size="49" font-weight="600" letter-spacing="-0.7">JobTrack</text>
|
||||
<rect x="340" y="172" width="92" height="28" rx="14" fill="#11151D" stroke="#3ECFAE" stroke-opacity="0.4"/>
|
||||
<circle cx="358" cy="186" r="3.5" fill="#3ECFAE"/>
|
||||
<text x="370" y="191" fill="#3ECFAE" font-family="Consolas, monospace" font-size="11">ACTIVE</text>
|
||||
<text x="120" y="248" fill="#9AA4B2" font-size="18">A job-application workspace covering the full journey from application to follow-up.</text>
|
||||
|
||||
<g font-family="Consolas, monospace" font-size="12" fill="#E8ECF2">
|
||||
<rect x="120" y="276" width="72" height="28" rx="14" fill="#171C26"/><text x="156" y="294" text-anchor="middle">.NET 9</text>
|
||||
<rect x="200" y="276" width="66" height="28" rx="14" fill="#171C26"/><text x="233" y="294" text-anchor="middle">React</text>
|
||||
<rect x="274" y="276" width="110" height="28" rx="14" fill="#171C26"/><text x="329" y="294" text-anchor="middle">TypeScript</text>
|
||||
<rect x="392" y="276" width="76" height="28" rx="14" fill="#171C26"/><text x="430" y="294" text-anchor="middle">SQLite</text>
|
||||
<rect x="476" y="276" width="86" height="28" rx="14" fill="#171C26"/><text x="519" y="294" text-anchor="middle">FastAPI</text>
|
||||
<rect x="570" y="276" width="76" height="28" rx="14" fill="#171C26"/><text x="608" y="294" text-anchor="middle">Ollama</text>
|
||||
<rect x="654" y="276" width="76" height="28" rx="14" fill="#171C26"/><text x="692" y="294" text-anchor="middle">Docker</text>
|
||||
</g>
|
||||
<text x="1200" y="294" fill="#3ECFAE" font-size="14">Repository ↗</text>
|
||||
|
||||
<!-- hero screenshot: browser frame -->
|
||||
<rect x="120" y="336" width="1200" height="520" rx="16" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<rect x="120" y="336" width="1200" height="40" rx="16" fill="#11151D"/>
|
||||
<rect x="120" y="360" width="1200" height="16" fill="#11151D"/>
|
||||
<circle cx="148" cy="356" r="5" fill="#5C6675"/><circle cx="166" cy="356" r="5" fill="#5C6675"/><circle cx="184" cy="356" r="5" fill="#5C6675"/>
|
||||
<rect x="560" y="346" width="320" height="20" rx="10" fill="#171C26"/>
|
||||
<text x="720" y="360" fill="#5C6675" font-family="Consolas, monospace" font-size="11" text-anchor="middle">jobtrack.local</text>
|
||||
<!-- mock app content -->
|
||||
<rect x="150" y="400" width="220" height="426" rx="8" fill="#11151D"/>
|
||||
<rect x="390" y="400" width="900" height="60" rx="8" fill="#11151D"/>
|
||||
<text x="410" y="436" fill="#9AA4B2" font-size="14">Applications · filters · search</text>
|
||||
<g fill="#11151D">
|
||||
<rect x="390" y="474" width="900" height="52" rx="6"/>
|
||||
<rect x="390" y="534" width="900" height="52" rx="6"/>
|
||||
<rect x="390" y="594" width="900" height="52" rx="6"/>
|
||||
<rect x="390" y="654" width="900" height="52" rx="6"/>
|
||||
<rect x="390" y="714" width="900" height="52" rx="6"/>
|
||||
</g>
|
||||
<g font-family="Consolas, monospace" font-size="11">
|
||||
<rect x="1160" y="488" width="92" height="24" rx="12" fill="#0F2B24"/><text x="1206" y="504" fill="#3ECFAE" text-anchor="middle">INTERVIEW</text>
|
||||
<rect x="1160" y="548" width="92" height="24" rx="12" fill="#2E2410"/><text x="1206" y="564" fill="#E5A93D" text-anchor="middle">WAITING</text>
|
||||
<rect x="1160" y="608" width="92" height="24" rx="12" fill="#171C26"/><text x="1206" y="624" fill="#9AA4B2" text-anchor="middle">APPLIED</text>
|
||||
</g>
|
||||
<text x="720" y="800" fill="#5C6675" font-size="13" text-anchor="middle">[ 2× capture: applications table with status filters — seeded demo data ]</text>
|
||||
|
||||
<!-- TL;DR box -->
|
||||
<rect x="120" y="896" width="800" height="180" rx="12" fill="#171C26"/>
|
||||
<rect x="120" y="896" width="3" height="180" fill="#3ECFAE"/>
|
||||
<text x="148" y="932" fill="#5C6675" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">TL;DR</text>
|
||||
<text x="148" y="964" fill="#E8ECF2" font-size="15"><tspan font-weight="600">What</tspan> Full-stack job-application tracker: pipeline, correspondence, attachments, analytics</text>
|
||||
<text x="148" y="992" fill="#E8ECF2" font-size="15"><tspan font-weight="600">Why</tspan> Real problem — my own job search needed production-grade tooling</text>
|
||||
<text x="148" y="1020" fill="#E8ECF2" font-size="15"><tspan font-weight="600">Stack</tspan> React + ASP.NET Core + SQLite · FastAPI/Ollama AI service · Docker Compose</text>
|
||||
<text x="148" y="1048" fill="#E8ECF2" font-size="15"><tspan font-weight="600">Role</tspan> Everything: product, backend, frontend, ops, security</text>
|
||||
|
||||
<!-- mini TOC -->
|
||||
<g font-size="13">
|
||||
<text x="1080" y="932" fill="#5C6675" font-family="Consolas, monospace" font-size="11" letter-spacing="1.5">ON THIS PAGE</text>
|
||||
<text x="1080" y="964" fill="#9AA4B2">Problem</text>
|
||||
<rect x="1064" y="982" width="2" height="16" fill="#3ECFAE"/>
|
||||
<text x="1080" y="994" fill="#E8ECF2">Architecture</text>
|
||||
<text x="1080" y="1024" fill="#9AA4B2">Decisions</text>
|
||||
<text x="1080" y="1054" fill="#9AA4B2">Security</text>
|
||||
<text x="1080" y="1084" fill="#9AA4B2">Screenshots</text>
|
||||
<text x="1080" y="1114" fill="#9AA4B2">What's next</text>
|
||||
</g>
|
||||
|
||||
<!-- Architecture section -->
|
||||
<text x="120" y="1160" fill="#3ECFAE" font-family="Consolas, monospace" font-size="12" letter-spacing="2">ARCHITECTURE</text>
|
||||
<text x="120" y="1204" fill="#E8ECF2" font-size="31" font-weight="600">Four services, one compose file</text>
|
||||
|
||||
<!-- diagram -->
|
||||
<rect x="120" y="1240" width="1200" height="300" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<!-- nodes -->
|
||||
<g>
|
||||
<rect x="170" y="1330" width="180" height="72" rx="8" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="260" y="1360" fill="#E8ECF2" font-size="14" text-anchor="middle">React SPA</text>
|
||||
<text x="260" y="1382" fill="#5C6675" font-family="Consolas, monospace" font-size="11" text-anchor="middle">nginx · PWA</text>
|
||||
|
||||
<rect x="440" y="1330" width="200" height="72" rx="8" fill="#171C26" stroke="#3ECFAE" stroke-opacity="0.5"/>
|
||||
<text x="540" y="1360" fill="#E8ECF2" font-size="14" text-anchor="middle">ASP.NET Core API</text>
|
||||
<text x="540" y="1382" fill="#5C6675" font-family="Consolas, monospace" font-size="11" text-anchor="middle">.NET 9 · auth · rules</text>
|
||||
|
||||
<rect x="730" y="1276" width="180" height="64" rx="8" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="820" y="1302" fill="#E8ECF2" font-size="14" text-anchor="middle">SQLite + files</text>
|
||||
<text x="820" y="1322" fill="#5C6675" font-family="Consolas, monospace" font-size="11" text-anchor="middle">EF Core · attachments</text>
|
||||
|
||||
<rect x="730" y="1364" width="180" height="64" rx="8" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="820" y="1390" fill="#E8ECF2" font-size="14" text-anchor="middle">AI service</text>
|
||||
<text x="820" y="1410" fill="#5C6675" font-family="Consolas, monospace" font-size="11" text-anchor="middle">FastAPI → Ollama</text>
|
||||
|
||||
<rect x="1000" y="1330" width="180" height="72" rx="8" fill="#171C26" stroke="#E5A93D" stroke-opacity="0.5"/>
|
||||
<text x="1090" y="1360" fill="#E8ECF2" font-size="14" text-anchor="middle">Gmail API</text>
|
||||
<text x="1090" y="1382" fill="#8A6114" font-family="Consolas, monospace" font-size="11" text-anchor="middle">external · OAuth2</text>
|
||||
</g>
|
||||
<!-- edges -->
|
||||
<g stroke="#3ECFAE" stroke-width="1.5" fill="none">
|
||||
<path d="M350 1366 H440"/>
|
||||
<path d="M640 1352 L730 1308"/>
|
||||
<path d="M640 1380 L730 1396"/>
|
||||
</g>
|
||||
<path d="M640 1366 H960 M960 1366 H1000" stroke="#E5A93D" stroke-width="1.5" stroke-dasharray="4 4" fill="none"/>
|
||||
<text x="380" y="1356" fill="#5C6675" font-family="Consolas, monospace" font-size="10">/api</text>
|
||||
<text x="930" y="1356" fill="#8A6114" font-family="Consolas, monospace" font-size="10">import</text>
|
||||
<text x="148" y="1276" fill="#5C6675" font-family="Consolas, monospace" font-size="11">hover: node highlights its edges, others dim to 60% (desktop only — fully legible static)</text>
|
||||
|
||||
<!-- decisions preview -->
|
||||
<text x="120" y="1600" fill="#3ECFAE" font-family="Consolas, monospace" font-size="12" letter-spacing="2">KEY DECISIONS & TRADE-OFFS</text>
|
||||
<text x="120" y="1644" fill="#E8ECF2" font-size="16"><tspan fill="#3ECFAE" font-family="Consolas, monospace">01</tspan> Local AI (Ollama) over a cloud API — privacy of job-search data + zero per-call cost, traded against setup complexity.</text>
|
||||
<text x="120" y="1676" fill="#E8ECF2" font-size="16"><tspan fill="#3ECFAE" font-family="Consolas, monospace">02</tspan> No offline cache in the PWA — deliberate: frequent deploys made stale builds a worse failure than offline gaps.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,133 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 1240" font-family="Segoe UI, Arial, sans-serif">
|
||||
<rect width="900" height="1240" fill="#05070B"/>
|
||||
<text x="60" y="48" fill="#E5A93D" font-family="Consolas, monospace" font-size="12">M4: mobile 390px — homepage (left) · nav overlay open (right)</text>
|
||||
|
||||
<!-- ============ PHONE 1: homepage ============ -->
|
||||
<rect x="60" y="80" width="390" height="1100" rx="24" fill="#0B0E14" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
|
||||
<!-- slim header 56 -->
|
||||
<line x1="60" y1="136" x2="450" y2="136" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="84" y="94" width="28" height="28" rx="7" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="98" y="113" fill="#E8ECF2" font-family="Consolas, monospace" font-size="11" font-weight="bold" text-anchor="middle">CB</text>
|
||||
<!-- CV icon button -->
|
||||
<rect x="330" y="94" width="40" height="28" rx="7" fill="#3ECFAE"/>
|
||||
<path d="M350 100 v10 m-4 -4 l4 4 l4 -4 M344 114 h12" stroke="#062A22" stroke-width="1.6" fill="none"/>
|
||||
<!-- burger -->
|
||||
<g stroke="#E8ECF2" stroke-width="1.6">
|
||||
<line x1="390" y1="102" x2="414" y2="102"/><line x1="390" y1="110" x2="414" y2="110"/><line x1="390" y1="118" x2="414" y2="118"/>
|
||||
</g>
|
||||
|
||||
<!-- hero -->
|
||||
<text x="84" y="192" fill="#3ECFAE" font-family="Consolas, monospace" font-size="10" letter-spacing="1.5">SYSTEMS DEVELOPER · NORWAY</text>
|
||||
<text x="84" y="234" fill="#E8ECF2" font-size="34" font-weight="600">Connor</text>
|
||||
<text x="84" y="272" fill="#E8ECF2" font-size="34" font-weight="600">Babbington</text>
|
||||
<text x="84" y="308" fill="#9AA4B2" font-size="15">I design, build and run web systems.</text>
|
||||
<text x="84" y="332" fill="#9AA4B2" font-size="15">8+ years in UK local government —</text>
|
||||
<text x="84" y="356" fill="#9AA4B2" font-size="15">now building full-stack in Norway.</text>
|
||||
<path d="M84 384 H240 l8 8 H330" fill="none" stroke="#3ECFAE" stroke-width="1.5"/>
|
||||
<circle cx="335" cy="392" r="3.5" fill="#3ECFAE"/>
|
||||
|
||||
<!-- chips 2 rows -->
|
||||
<g font-family="Consolas, monospace" font-size="10.5">
|
||||
<rect x="84" y="410" width="128" height="26" rx="13" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="148" y="427" fill="#9AA4B2" text-anchor="middle">Tønsberg, Norway</text>
|
||||
<rect x="220" y="410" width="108" height="26" rx="13" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="274" y="427" fill="#9AA4B2" text-anchor="middle">Work permit ✓</text>
|
||||
<rect x="84" y="444" width="150" height="26" rx="13" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="159" y="461" fill="#9AA4B2" text-anchor="middle">Remote/hybrid/on-site</text>
|
||||
<rect x="242" y="444" width="150" height="26" rx="13" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="317" y="461" fill="#9AA4B2" text-anchor="middle">EN native · NO B1</text>
|
||||
</g>
|
||||
|
||||
<!-- stacked CTAs full width, 48px -->
|
||||
<rect x="84" y="494" width="342" height="48" rx="8" fill="#3ECFAE"/>
|
||||
<text x="255" y="524" fill="#062A22" font-size="15" font-weight="600" text-anchor="middle">View projects</text>
|
||||
<rect x="84" y="552" width="342" height="48" rx="8" fill="none" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="255" y="582" fill="#E8ECF2" font-size="15" text-anchor="middle">Download CV · pdf</text>
|
||||
|
||||
<!-- proof 2x2 -->
|
||||
<g>
|
||||
<rect x="84" y="628" width="165" height="72" rx="10" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="100" y="660" fill="#E8ECF2" font-size="18" font-weight="600">8+ yrs</text>
|
||||
<text x="100" y="682" fill="#5C6675" font-family="Consolas, monospace" font-size="9" letter-spacing="1">EXPERIENCE</text>
|
||||
<rect x="261" y="628" width="165" height="72" rx="10" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="277" y="660" fill="#E8ECF2" font-size="15" font-weight="600">Public sector</text>
|
||||
<text x="277" y="682" fill="#5C6675" font-family="Consolas, monospace" font-size="9" letter-spacing="1">BACKGROUND</text>
|
||||
<rect x="84" y="710" width="165" height="72" rx="10" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="100" y="742" fill="#E8ECF2" font-size="15" font-weight="600">.NET · React</text>
|
||||
<text x="100" y="764" fill="#5C6675" font-family="Consolas, monospace" font-size="9" letter-spacing="1">CORE STACK</text>
|
||||
<rect x="261" y="710" width="165" height="72" rx="10" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<text x="277" y="742" fill="#E8ECF2" font-size="15" font-weight="600">In Norway</text>
|
||||
<text x="277" y="764" fill="#5C6675" font-family="Consolas, monospace" font-size="9" letter-spacing="1">PERMIT ✓</text>
|
||||
</g>
|
||||
|
||||
<!-- project card -->
|
||||
<text x="84" y="836" fill="#3ECFAE" font-family="Consolas, monospace" font-size="10" letter-spacing="1.5">02 — PROJECTS</text>
|
||||
<rect x="84" y="856" width="342" height="290" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="84" y="856" width="342" height="170" rx="12" fill="#171C26"/>
|
||||
<rect x="84" y="1010" width="342" height="16" fill="#171C26"/>
|
||||
<text x="255" y="948" fill="#5C6675" font-size="11" text-anchor="middle">[ JobTrack screenshot 16:10 ]</text>
|
||||
<rect x="342" y="872" width="70" height="24" rx="12" fill="#0B0E14" stroke="#3ECFAE" stroke-opacity="0.4"/>
|
||||
<circle cx="356" cy="884" r="3" fill="#3ECFAE"/>
|
||||
<text x="366" y="889" fill="#3ECFAE" font-family="Consolas, monospace" font-size="9.5">ACTIVE</text>
|
||||
<text x="104" y="1058" fill="#E8ECF2" font-size="18" font-weight="600">JobTrack</text>
|
||||
<text x="104" y="1082" fill="#9AA4B2" font-size="13">A workspace for the whole job-application journey.</text>
|
||||
<g font-family="Consolas, monospace" font-size="10" fill="#9AA4B2">
|
||||
<rect x="104" y="1096" width="56" height="22" rx="11" fill="#171C26"/><text x="132" y="1111" text-anchor="middle">.NET 9</text>
|
||||
<rect x="168" y="1096" width="52" height="22" rx="11" fill="#171C26"/><text x="194" y="1111" text-anchor="middle">React</text>
|
||||
<rect x="228" y="1096" width="60" height="22" rx="11" fill="#171C26"/><text x="258" y="1111" text-anchor="middle">Docker</text>
|
||||
</g>
|
||||
<text x="104" y="1136" fill="#3ECFAE" font-size="13">Read case study →</text>
|
||||
|
||||
<!-- ============ PHONE 2: nav overlay ============ -->
|
||||
<rect x="480" y="80" width="390" height="1100" rx="24" fill="#0B0E14" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<rect x="480" y="80" width="390" height="1100" rx="24" fill="#11151D" fill-opacity="0.98"/>
|
||||
|
||||
<!-- overlay header -->
|
||||
<rect x="504" y="94" width="28" height="28" rx="7" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="518" y="113" fill="#E8ECF2" font-family="Consolas, monospace" font-size="11" font-weight="bold" text-anchor="middle">CB</text>
|
||||
<g stroke="#E8ECF2" stroke-width="1.8">
|
||||
<line x1="812" y1="98" x2="832" y2="118"/><line x1="832" y1="98" x2="812" y2="118"/>
|
||||
</g>
|
||||
|
||||
<!-- language + theme row -->
|
||||
<rect x="504" y="152" width="240" height="44" rx="22" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="508" y="156" width="116" height="36" rx="18" fill="#0B0E14" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="566" y="179" fill="#E8ECF2" font-family="Consolas, monospace" font-size="13" text-anchor="middle">EN</text>
|
||||
<text x="686" y="179" fill="#9AA4B2" font-family="Consolas, monospace" font-size="13" text-anchor="middle">NO</text>
|
||||
<circle cx="800" cy="174" r="22" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<path d="M806 166 a9 9 0 1 0 4 12 a8 8 0 0 1 -4 -12" fill="#9AA4B2"/>
|
||||
|
||||
<!-- nav stack: large targets -->
|
||||
<g font-size="28" font-weight="600">
|
||||
<text x="504" y="292" fill="#E8ECF2">Home</text>
|
||||
<text x="504" y="368" fill="#E8ECF2">Projects</text>
|
||||
<text x="504" y="444" fill="#E8ECF2">Experience</text>
|
||||
<text x="504" y="520" fill="#E8ECF2">About</text>
|
||||
<text x="504" y="596" fill="#E8ECF2">Contact</text>
|
||||
</g>
|
||||
<g font-family="Consolas, monospace" font-size="11" fill="#5C6675">
|
||||
<text x="504" y="316">/</text>
|
||||
<text x="504" y="392">/projects · 3</text>
|
||||
<text x="504" y="468">/experience</text>
|
||||
<text x="504" y="544">/about</text>
|
||||
<text x="504" y="620">/contact</text>
|
||||
</g>
|
||||
<line x1="504" y1="660" x2="846" y2="660" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
|
||||
<!-- CV buttons -->
|
||||
<rect x="504" y="692" width="342" height="48" rx="8" fill="#3ECFAE"/>
|
||||
<text x="675" y="722" fill="#062A22" font-size="15" font-weight="600" text-anchor="middle">Download CV — English</text>
|
||||
<rect x="504" y="752" width="342" height="48" rx="8" fill="none" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="675" y="782" fill="#E8ECF2" font-size="15" text-anchor="middle">Last ned CV — Norsk</text>
|
||||
|
||||
<!-- contact shortcuts -->
|
||||
<g font-family="Consolas, monospace" font-size="13" fill="#9AA4B2">
|
||||
<text x="504" y="852">connor.babbington@cesnimda.co.uk ⧉</text>
|
||||
<text x="504" y="888">linkedin ↗</text>
|
||||
<text x="504" y="924">git.cesnimda.uk ↗</text>
|
||||
</g>
|
||||
|
||||
<text x="504" y="1150" fill="#5C6675" font-family="Consolas, monospace" font-size="11">overlay: full-screen, body scroll locked, Esc/× closes,</text>
|
||||
<text x="504" y="1168" fill="#5C6675" font-family="Consolas, monospace" font-size="11">focus trapped · targets ≥ 44px</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
@@ -0,0 +1,132 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 1040" font-family="Segoe UI, Arial, sans-serif">
|
||||
<rect width="1440" height="1040" fill="#05070B"/>
|
||||
<text x="60" y="44" fill="#E5A93D" font-family="Consolas, monospace" font-size="12">M5: navigation & control states</text>
|
||||
|
||||
<!-- (a) default header -->
|
||||
<text x="60" y="86" fill="#5C6675" font-family="Consolas, monospace" font-size="11">(a) default · 72px</text>
|
||||
<rect x="60" y="100" width="1320" height="72" fill="#0B0E14" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="90" y="120" width="32" height="32" rx="8" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="106" y="141" fill="#E8ECF2" font-family="Consolas, monospace" font-size="13" font-weight="bold" text-anchor="middle">CB</text>
|
||||
<text x="380" y="141" fill="#9AA4B2" font-size="14">Projects</text>
|
||||
<text x="470" y="141" fill="#9AA4B2" font-size="14">Experience</text>
|
||||
<text x="580" y="141" fill="#9AA4B2" font-size="14">About</text>
|
||||
<text x="655" y="141" fill="#9AA4B2" font-size="14">Contact</text>
|
||||
<rect x="960" y="122" width="88" height="28" rx="14" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="963" y="125" width="41" height="22" rx="11" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="983" y="140" fill="#E8ECF2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">EN</text>
|
||||
<text x="1026" y="140" fill="#9AA4B2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">NO</text>
|
||||
<circle cx="1082" cy="136" r="14" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="1116" y="120" width="164" height="32" rx="8" fill="#3ECFAE"/>
|
||||
<text x="1176" y="141" fill="#062A22" font-size="13" font-weight="600" text-anchor="middle">Download CV</text>
|
||||
<line x1="1250" y1="126" x2="1250" y2="146" stroke="#062A22" stroke-opacity="0.3"/>
|
||||
<path d="M1258 133 l6 6 l6 -6" fill="none" stroke="#062A22" stroke-width="1.8"/>
|
||||
|
||||
<!-- (b) scrolled -->
|
||||
<text x="60" y="216" fill="#5C6675" font-family="Consolas, monospace" font-size="11">(b) scrolled · 56px · backdrop blur + hairline · hover on "Projects" (underline draw) · current page "About"</text>
|
||||
<rect x="60" y="230" width="1320" height="56" fill="#0B0E14" fill-opacity="0.85" stroke="#E8ECF2" stroke-opacity="0.12"/>
|
||||
<rect x="90" y="244" width="28" height="28" rx="7" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="104" y="263" fill="#E8ECF2" font-family="Consolas, monospace" font-size="12" font-weight="bold" text-anchor="middle">CB</text>
|
||||
<text x="380" y="264" fill="#E8ECF2" font-size="13">Projects</text>
|
||||
<line x1="380" y1="271" x2="410" y2="271" stroke="#3ECFAE" stroke-width="2"/>
|
||||
<text x="470" y="264" fill="#9AA4B2" font-size="13">Experience</text>
|
||||
<text x="570" y="264" fill="#E8ECF2" font-size="13">About</text>
|
||||
<line x1="570" y1="271" x2="608" y2="271" stroke="#3ECFAE" stroke-width="2" stroke-opacity="0.6"/>
|
||||
<text x="645" y="264" fill="#9AA4B2" font-size="13">Contact</text>
|
||||
<rect x="1136" y="243" width="144" height="30" rx="8" fill="#3ECFAE"/>
|
||||
<text x="1198" y="263" fill="#062A22" font-size="12" font-weight="600" text-anchor="middle">Download CV ▾</text>
|
||||
|
||||
<!-- (c) CV split open -->
|
||||
<text x="60" y="336" fill="#5C6675" font-family="Consolas, monospace" font-size="11">(c) CV split-button open · locale default first · file sizes · keyboard: ↓↑ Esc</text>
|
||||
<rect x="60" y="350" width="164" height="32" rx="8" fill="#3ECFAE"/>
|
||||
<text x="120" y="371" fill="#062A22" font-size="13" font-weight="600" text-anchor="middle">Download CV</text>
|
||||
<line x1="194" y1="356" x2="194" y2="376" stroke="#062A22" stroke-opacity="0.3"/>
|
||||
<path d="M202 366 l6 -6 l6 6" fill="none" stroke="#062A22" stroke-width="1.8"/>
|
||||
<rect x="60" y="392" width="280" height="104" rx="12" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<rect x="68" y="402" width="264" height="40" rx="8" fill="#11151D"/>
|
||||
<rect x="80" y="412" width="30" height="20" rx="10" fill="#0F2B24"/>
|
||||
<text x="95" y="426" fill="#3ECFAE" font-family="Consolas, monospace" font-size="10" text-anchor="middle">EN</text>
|
||||
<text x="122" y="427" fill="#E8ECF2" font-size="13">English CV</text>
|
||||
<text x="322" y="427" fill="#5C6675" font-family="Consolas, monospace" font-size="11" text-anchor="end">pdf · 180 kB</text>
|
||||
<rect x="80" y="454" width="30" height="20" rx="10" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="95" y="468" fill="#9AA4B2" font-family="Consolas, monospace" font-size="10" text-anchor="middle">NO</text>
|
||||
<text x="122" y="469" fill="#9AA4B2" font-size="13">Norsk CV</text>
|
||||
<text x="322" y="469" fill="#5C6675" font-family="Consolas, monospace" font-size="11" text-anchor="end">pdf · 172 kB</text>
|
||||
|
||||
<!-- (d) language switch states -->
|
||||
<text x="480" y="336" fill="#5C6675" font-family="Consolas, monospace" font-size="11">(d) language switch · EN active / NO active / focus-visible · thumb slides 160ms · aria toggle group</text>
|
||||
<g>
|
||||
<rect x="480" y="352" width="88" height="28" rx="14" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="483" y="355" width="41" height="22" rx="11" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="503" y="370" fill="#E8ECF2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">EN</text>
|
||||
<text x="546" y="370" fill="#9AA4B2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">NO</text>
|
||||
|
||||
<rect x="600" y="352" width="88" height="28" rx="14" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="644" y="355" width="41" height="22" rx="11" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="620" y="370" fill="#9AA4B2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">EN</text>
|
||||
<text x="664" y="370" fill="#E8ECF2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">NO</text>
|
||||
|
||||
<rect x="720" y="352" width="88" height="28" rx="14" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="718" y="350" width="92" height="32" rx="16" fill="none" stroke="#3ECFAE" stroke-width="2"/>
|
||||
<rect x="723" y="355" width="41" height="22" rx="11" fill="#171C26" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="743" y="370" fill="#E8ECF2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">EN</text>
|
||||
<text x="786" y="370" fill="#9AA4B2" font-family="Consolas, monospace" font-size="11" text-anchor="middle">NO</text>
|
||||
</g>
|
||||
|
||||
<!-- (e) first-visit language hint -->
|
||||
<text x="480" y="430" fill="#5C6675" font-family="Consolas, monospace" font-size="11">(e) first-visit hint (browser prefers nb-NO) · shown once · dismissible · never auto-redirects</text>
|
||||
<rect x="480" y="444" width="620" height="44" rx="8" fill="#171C26" stroke="#3ECFAE" stroke-opacity="0.3"/>
|
||||
<text x="500" y="471" fill="#E8ECF2" font-size="13">Denne siden finnes på norsk</text>
|
||||
<rect x="700" y="452" width="66" height="28" rx="8" fill="#3ECFAE"/>
|
||||
<text x="733" y="471" fill="#062A22" font-size="12" font-weight="600" text-anchor="middle">Bytt</text>
|
||||
<text x="1076" y="472" fill="#9AA4B2" font-size="14" text-anchor="end">✕</text>
|
||||
|
||||
<!-- (f) buttons row -->
|
||||
<text x="60" y="560" fill="#5C6675" font-family="Consolas, monospace" font-size="11">(f) buttons: primary rest/hover/focus · secondary · ghost · disabled</text>
|
||||
<rect x="60" y="576" width="150" height="44" rx="8" fill="#3ECFAE"/>
|
||||
<text x="135" y="603" fill="#062A22" font-size="14" font-weight="600" text-anchor="middle">Primary</text>
|
||||
<rect x="230" y="572" width="150" height="44" rx="8" fill="#54DBBC"/>
|
||||
<text x="305" y="599" fill="#062A22" font-size="14" font-weight="600" text-anchor="middle">Hover +1px ↑</text>
|
||||
<rect x="400" y="576" width="150" height="44" rx="8" fill="#3ECFAE"/>
|
||||
<rect x="396" y="572" width="158" height="52" rx="10" fill="none" stroke="#3ECFAE" stroke-width="2"/>
|
||||
<text x="475" y="603" fill="#062A22" font-size="14" font-weight="600" text-anchor="middle">Focus ring</text>
|
||||
<rect x="590" y="576" width="150" height="44" rx="8" fill="none" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="665" y="603" fill="#E8ECF2" font-size="14" text-anchor="middle">Secondary</text>
|
||||
<text x="790" y="603" fill="#9AA4B2" font-size="14">Ghost link</text>
|
||||
<line x1="790" y1="610" x2="856" y2="610" stroke="#3ECFAE" stroke-width="2"/>
|
||||
<rect x="900" y="576" width="150" height="44" rx="8" fill="#171C26"/>
|
||||
<text x="975" y="603" fill="#5C6675" font-size="14" text-anchor="middle">Disabled</text>
|
||||
|
||||
<!-- (g) chips & status -->
|
||||
<text x="60" y="690" fill="#5C6675" font-family="Consolas, monospace" font-size="11">(g) status semantics: accent=active · amber=in development · muted=archived (dot + label, never colour-only)</text>
|
||||
<g font-family="Consolas, monospace" font-size="11">
|
||||
<rect x="60" y="706" width="92" height="28" rx="14" fill="#11151D" stroke="#3ECFAE" stroke-opacity="0.4"/>
|
||||
<circle cx="78" cy="720" r="3.5" fill="#3ECFAE"/>
|
||||
<text x="90" y="725" fill="#3ECFAE">ACTIVE</text>
|
||||
<rect x="168" y="706" width="150" height="28" rx="14" fill="#11151D" stroke="#E5A93D" stroke-opacity="0.4"/>
|
||||
<circle cx="186" cy="720" r="3.5" fill="#E5A93D"/>
|
||||
<text x="198" y="725" fill="#E5A93D">IN DEVELOPMENT</text>
|
||||
<rect x="334" y="706" width="104" height="28" rx="14" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<circle cx="352" cy="720" r="3.5" fill="#5C6675"/>
|
||||
<text x="364" y="725" fill="#9AA4B2">ARCHIVED</text>
|
||||
</g>
|
||||
|
||||
<!-- (h) form states -->
|
||||
<text x="60" y="800" fill="#5C6675" font-family="Consolas, monospace" font-size="11">(h) form: rest · focus · error (icon + message, danger ≥4.5:1) · submit-sending · success panel</text>
|
||||
<text x="60" y="832" fill="#9AA4B2" font-size="13">E-mail</text>
|
||||
<rect x="60" y="842" width="260" height="44" rx="8" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<text x="340" y="832" fill="#E8ECF2" font-size="13">E-mail</text>
|
||||
<rect x="340" y="842" width="260" height="44" rx="8" fill="#11151D" stroke="#3ECFAE" stroke-width="2"/>
|
||||
<text x="620" y="832" fill="#E8ECF2" font-size="13">E-mail</text>
|
||||
<rect x="620" y="842" width="260" height="44" rx="8" fill="#11151D" stroke="#E5606B" stroke-width="2"/>
|
||||
<text x="620" y="908" fill="#E5606B" font-size="12">⚠ Please enter a valid e-mail address</text>
|
||||
<rect x="920" y="842" width="180" height="44" rx="8" fill="#3ECFAE"/>
|
||||
<text x="1010" y="869" fill="#062A22" font-family="Consolas, monospace" font-size="14" text-anchor="middle">···</text>
|
||||
<rect x="1130" y="830" width="250 " height="70" rx="12" fill="#171C26" stroke="#3ECFAE" stroke-opacity="0.4"/>
|
||||
<text x="1156" y="860" fill="#3ECFAE" font-size="14">✓ Message sent</text>
|
||||
<text x="1156" y="882" fill="#9AA4B2" font-size="12">I usually reply within a day.</text>
|
||||
|
||||
<!-- (i) skip link -->
|
||||
<text x="60" y="972" fill="#5C6675" font-family="Consolas, monospace" font-size="11">(i) skip-link, visible on first Tab</text>
|
||||
<rect x="60" y="984" width="220" height="40" rx="8" fill="#3ECFAE"/>
|
||||
<text x="170" y="1009" fill="#062A22" font-size="13" font-weight="600" text-anchor="middle">Skip to content ↵</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,91 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 1560" font-family="Segoe UI, Arial, sans-serif">
|
||||
<rect width="1440" height="1560" fill="#0B0E14"/>
|
||||
<text x="120" y="44" fill="#E5A93D" font-family="Consolas, monospace" font-size="12">M6: homepage lower half — projects · experience timeline · about teaser · contact band</text>
|
||||
|
||||
<!-- ===== PROJECTS ===== -->
|
||||
<text x="120" y="110" fill="#3ECFAE" font-family="Consolas, monospace" font-size="12" letter-spacing="2">02 — PROJECTS</text>
|
||||
<text x="120" y="162" fill="#E8ECF2" font-size="39" font-weight="600">Built, shipped, and running</text>
|
||||
|
||||
<!-- card 1: JobTrack (hover state shown) -->
|
||||
<rect x="120" y="200" width="588" height="470" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.16"/>
|
||||
<rect x="120" y="200" width="588" height="280" rx="12" fill="#171C26"/>
|
||||
<rect x="120" y="464" width="588" height="16" fill="#171C26"/>
|
||||
<text x="414" y="348" fill="#5C6675" font-size="12" text-anchor="middle">[ JobTrack — applications table screenshot, 4px hover shift ]</text>
|
||||
<rect x="600" y="220" width="88" height="26" rx="13" fill="#0B0E14" stroke="#3ECFAE" stroke-opacity="0.4"/>
|
||||
<circle cx="616" cy="233" r="3.5" fill="#3ECFAE"/>
|
||||
<text x="628" y="238" fill="#3ECFAE" font-family="Consolas, monospace" font-size="10">ACTIVE</text>
|
||||
<text x="148" y="524" fill="#E8ECF2" font-size="24" font-weight="600">JobTrack</text>
|
||||
<line x1="148" y1="532" x2="248" y2="532" stroke="#3ECFAE" stroke-width="2"/>
|
||||
<text x="148" y="556" fill="#9AA4B2" font-size="14">One workspace for the whole job-application journey —</text>
|
||||
<text x="148" y="576" fill="#9AA4B2" font-size="14">pipeline, correspondence, attachments, AI summaries.</text>
|
||||
<g font-family="Consolas, monospace" font-size="11" fill="#9AA4B2">
|
||||
<rect x="148" y="594" width="60" height="24" rx="12" fill="#171C26"/><text x="178" y="610" text-anchor="middle">.NET 9</text>
|
||||
<rect x="216" y="594" width="56" height="24" rx="12" fill="#171C26"/><text x="244" y="610" text-anchor="middle">React</text>
|
||||
<rect x="280" y="594" width="66" height="24" rx="12" fill="#171C26"/><text x="313" y="610" text-anchor="middle">SQLite</text>
|
||||
<rect x="354" y="594" width="66" height="24" rx="12" fill="#171C26"/><text x="387" y="610" text-anchor="middle">Ollama</text>
|
||||
<rect x="428" y="594" width="66" height="24" rx="12" fill="#171C26"/><text x="461" y="610" text-anchor="middle">Docker</text>
|
||||
</g>
|
||||
<text x="148" y="646" fill="#3ECFAE" font-size="14">Read case study →→</text>
|
||||
|
||||
<!-- card 2: InboxIntel (rest state) -->
|
||||
<rect x="732" y="200" width="588" height="470" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<rect x="732" y="200" width="588" height="280" rx="12" fill="#171C26"/>
|
||||
<rect x="732" y="464" width="588" height="16" fill="#171C26"/>
|
||||
<text x="1026" y="348" fill="#5C6675" font-size="12" text-anchor="middle">[ InboxIntel — draggable analytics dashboard screenshot ]</text>
|
||||
<rect x="1156" y="220" width="146" height="26" rx="13" fill="#0B0E14" stroke="#E5A93D" stroke-opacity="0.4"/>
|
||||
<circle cx="1172" cy="233" r="3.5" fill="#E5A93D"/>
|
||||
<text x="1184" y="238" fill="#E5A93D" font-family="Consolas, monospace" font-size="10">IN DEVELOPMENT</text>
|
||||
<text x="760" y="524" fill="#E8ECF2" font-size="24" font-weight="600">InboxIntel</text>
|
||||
<text x="760" y="556" fill="#9AA4B2" font-size="14">Gmail analytics and safe bulk cleanup — Clean Architecture,</text>
|
||||
<text x="760" y="576" fill="#9AA4B2" font-size="14">encrypted OAuth tokens, preview-before-delete by design.</text>
|
||||
<g font-family="Consolas, monospace" font-size="11" fill="#9AA4B2">
|
||||
<rect x="760" y="594" width="60" height="24" rx="12" fill="#171C26"/><text x="790" y="610" text-anchor="middle">.NET 8</text>
|
||||
<rect x="828" y="594" width="100" height="24" rx="12" fill="#171C26"/><text x="878" y="610" text-anchor="middle">PostgreSQL</text>
|
||||
<rect x="936" y="594" width="56" height="24" rx="12" fill="#171C26"/><text x="964" y="610" text-anchor="middle">React</text>
|
||||
<rect x="1000" y="594" width="96" height="24" rx="12" fill="#171C26"/><text x="1048" y="610" text-anchor="middle">Gmail API</text>
|
||||
</g>
|
||||
<text x="760" y="646" fill="#3ECFAE" font-size="14">Read case study →</text>
|
||||
|
||||
<!-- slim homelab card -->
|
||||
<rect x="120" y="694" width="1200" height="92" rx="12" fill="#11151D" stroke="#E8ECF2" stroke-opacity="0.08"/>
|
||||
<g stroke="#3ECFAE" stroke-width="1.3" fill="none" opacity="0.8">
|
||||
<rect x="152" y="722" width="24" height="16" rx="2"/><rect x="152" y="744" width="24" height="16" rx="2"/>
|
||||
<path d="M176 730 h20 v22 h-20 M186 730 v-8 h30"/>
|
||||
</g>
|
||||
<text x="240" y="732" fill="#E8ECF2" font-size="18" font-weight="600">Self-hosted infrastructure lab</text>
|
||||
<text x="240" y="758" fill="#9AA4B2" font-size="14">Ubuntu · Docker services · reverse proxy · auth · monitoring · Gitea (this site's own git remote)</text>
|
||||
<text x="1180" y="746" fill="#3ECFAE" font-size="14">Explore →</text>
|
||||
|
||||
<!-- ===== EXPERIENCE ===== -->
|
||||
<text x="120" y="884" fill="#3ECFAE" font-family="Consolas, monospace" font-size="12" letter-spacing="2">03 — EXPERIENCE</text>
|
||||
<text x="120" y="936" fill="#E8ECF2" font-size="39" font-weight="600">Where I've worked</text>
|
||||
|
||||
<!-- timeline spine -->
|
||||
<line x1="160" y1="972" x2="160" y2="1270" stroke="#E8ECF2" stroke-opacity="0.12" stroke-width="1.5"/>
|
||||
<!-- featured -->
|
||||
<circle cx="160" cy="990" r="5" fill="#3ECFAE"/>
|
||||
<text x="196" y="996" fill="#E8ECF2" font-size="20" font-weight="600">System Developer — Warwickshire County Council</text>
|
||||
<text x="196" y="1020" fill="#5C6675" font-family="Consolas, monospace" font-size="12">2015 – 2023 · UK public sector · full-stack</text>
|
||||
<text x="196" y="1050" fill="#9AA4B2" font-size="15">· Replaced spreadsheet-heavy workflows with reliable internal systems used across the council</text>
|
||||
<text x="196" y="1076" fill="#9AA4B2" font-size="15">· Built and maintained full-stack applications in C#, Python, Ruby on Rails, SQL and JavaScript</text>
|
||||
<text x="196" y="1102" fill="#9AA4B2" font-size="15">· Owned deployments, permissions hardening and production troubleshooting on live systems</text>
|
||||
<!-- compact items -->
|
||||
<circle cx="160" cy="1150" r="4" fill="#5C6675"/>
|
||||
<text x="196" y="1156" fill="#9AA4B2" font-size="14">2017 – 2021 · Sales Representative · Royal Vapes</text>
|
||||
<circle cx="160" cy="1188" r="4" fill="#5C6675"/>
|
||||
<text x="196" y="1194" fill="#9AA4B2" font-size="14">2016 – 2018 · Bartender · The Hodcarrier</text>
|
||||
<circle cx="160" cy="1226" r="4" fill="#5C6675"/>
|
||||
<text x="196" y="1232" fill="#9AA4B2" font-size="14">2014 – 2015 · Receptionist · Nuffield Health</text>
|
||||
<circle cx="160" cy="1264" r="4" fill="#5C6675"/>
|
||||
<text x="196" y="1270" fill="#9AA4B2" font-size="14">2012 – 2015 · Extended Diploma NVQ L3 in ICT · Warwickshire College</text>
|
||||
<text x="196" y="1300" fill="#5C6675" font-family="Consolas, monospace" font-size="12">earlier customer-facing roles — kept for honesty, de-emphasised · timeline draws on scroll (A8)</text>
|
||||
|
||||
<!-- ===== CONTACT BAND ===== -->
|
||||
<rect x="120" y="1360" width="1200" height="140" rx="16" fill="#171C26"/>
|
||||
<text x="160" y="1418" fill="#E8ECF2" font-size="28" font-weight="600">Looking for a systems developer in Norway?</text>
|
||||
<text x="160" y="1450" fill="#9AA4B2" font-size="15">I usually reply within a day — e-mail is fastest.</text>
|
||||
<rect x="880" y="1404" width="300" height="48" rx="8" fill="#3ECFAE"/>
|
||||
<text x="1030" y="1434" fill="#062A22" font-size="15" font-weight="600" text-anchor="middle">connor.babbington@cesnimda.co.uk ⧉</text>
|
||||
<text x="1216" y="1434" fill="#062A22" font-size="0">copy</text>
|
||||
<text x="1230" y="1432" fill="#9AA4B2" font-size="14">Contact form →</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,116 @@
|
||||
# MOCKUPS.md
|
||||
Master mockup document. High-fidelity SVG boards live beside this file (`M1…M6.svg`); this document specifies every required state, including those not economical to board. All measurements reference SPACING_SYSTEM tokens; colours reference COLOUR_SYSTEM tokens.
|
||||
|
||||
Boards:
|
||||
- **M1** `M1-home-desktop-dark.svg` — homepage hero + proof strip + skills (dark, 1440)
|
||||
- **M2** `M2-home-desktop-light.svg` — same region, light theme
|
||||
- **M3** `M3-case-study-dark.svg` — JobTrack case-study page (dark, 1440)
|
||||
- **M4** `M4-mobile-dark.svg` — mobile homepage + open nav overlay (390)
|
||||
- **M5** `M5-header-states.svg` — navigation states, language switch, CV split button
|
||||
- **M6** `M6-projects-experience-contact.svg` — homepage lower half (projects cards, timeline, contact band)
|
||||
|
||||
---
|
||||
|
||||
## 1. Homepage — hero + narrative flow (M1/M2)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ HEADER 72px: [CB●] Projects Experience About Contact │ EN|NO ☾ [CV ▾] │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ (faint dot-grid + accent halo, top-left biased) │
|
||||
│ │
|
||||
│ mono-label: SYSTEMS DEVELOPER · TØNSBERG, NORWAY ┌──────────┐ │
|
||||
│ display: Connor Babbington │ portrait │ │
|
||||
│ h3-weight: I design, build and run web systems. │ hairline │ │
|
||||
│ body-lg: Eight-plus years delivering internal │ frame, │ │
|
||||
│ software for UK local government — now │ mono │ │
|
||||
│ building full-stack products in Norway. │ caption │ │
|
||||
│ ── trace line ──┐___________ ● status: open to work └──────────┘ │
|
||||
│ │
|
||||
│ chips: [Tønsberg, Norway] [Work permit ✓] [Remote/hybrid/on-site] │
|
||||
│ [English native · Norsk B1] │
|
||||
│ CTAs: [■ View projects] [□ Download CV · pdf] │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ PROOF STRIP (4 tiles, surface-1, hairline): 8+ YRS · PUBLIC SECTOR · │
|
||||
│ .NET/REACT/DOCKER · BASED IN NORWAY — mono labels, stat numerals │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
- Hero text column 7/12 cols; portrait 4/12, offset 1. Portrait: outdoor headshot, 4:5, radius 16, duotone in dark.
|
||||
- Scroll behaviour: header compresses at 120px; sections reveal per A2.
|
||||
- Hover states: CTAs per MICRO_INTERACTIONS buttons; portrait has no hover (non-interactive).
|
||||
|
||||
## 2. Skills section (M1 lower)
|
||||
|
||||
Three `surface-1` cards, 4/12 each, mono-labels `01 — DEVELOPMENT` / `02 — DEVOPS & INFRASTRUCTURE` / `03 — PRACTICES`; one context sentence (ink-muted) + chip rows. Card hover: border-strong. No proficiency bars.
|
||||
|
||||
## 3. Projects section — case-study cards (M6)
|
||||
|
||||
Two large cards (6/12 each): screenshot bleeding to top frame edge (browser-chrome mock), status chip overlaid top-right (`ACTIVE` accent / `IN DEVELOPMENT` amber), h3 title, 1-line problem statement, stack chips, ghost link `Read case study →`. Third slim full-width card for homelab: no screenshot; small topology glyph + one line. Hover per A3 (4px screenshot shift, border glow, arrow nudge).
|
||||
|
||||
## 4. Experience timeline (M6)
|
||||
|
||||
Left hairline spine with period dots. Featured item (System Developer, Warwickshire County Council, 2015–2023): expanded — role h4, employer + location mono-meta, 3 outcome bullets. Earlier roles: compact one-liners at 60% emphasis (`2017–2021 · Sales Representative · Royal Vapes` etc.). Education node at bottom. Timeline draws on entry (A8).
|
||||
|
||||
## 5. About section
|
||||
|
||||
7/12 text (3 short paragraphs per CONTENT_STRATEGY), 4/12 photo (same portrait, natural grade in light). Language chips repeated (`English — native` `Norsk — B1, aktiv utvikling`). Interests one mono-meta line, no icons.
|
||||
|
||||
## 6. Contact (M6 band + page)
|
||||
|
||||
Homepage band: `surface-2`, h2 "Looking for a systems developer in Norway?" / NO: "Ser dere etter en systemutvikler?", primary e-mail button (click-to-copy), secondary "Contact form →".
|
||||
Contact page: 6/12 form (name/e-mail/message, labels above, per MICRO_INTERACTIONS form), 5/12 direct channels card: e-mail (copy), LinkedIn, git.cesnimda.uk, response-time note. Success state per A9.
|
||||
|
||||
## 7. Navigation states (M5)
|
||||
|
||||
Boards show: (a) default header, (b) scrolled/compressed with blur, (c) CV split-button open (EN/NO menu with file sizes), (d) language switch EN-active vs NO-active with sliding thumb, (e) focus-visible rings on nav item + toggle, (f) mobile header + full-screen overlay (nav stack, language segmented control, theme toggle, CV buttons, contact shortcuts).
|
||||
|
||||
## 8. Mobile layouts (M4)
|
||||
|
||||
390px reference. Order: slim header (56px: monogram · CV icon-button · burger) → mono-label + name (40px) → summary → chips (wrap, 2 rows) → CTAs stacked full-width → proof strip 2×2 tiles → skills cards stacked → project cards full-width (screenshot 16:10) → timeline (spine indented 16px) → about → contact band → footer. Touch targets ≥44px; no parallax; reveals opacity-only.
|
||||
|
||||
## 9. Dark + light (M1 vs M2)
|
||||
|
||||
Identical geometry; token swap only. Light: shadows-card active on cards, dot-grid at 5% instead of 8%, accent `#0E8C72`, portrait natural. Boards demonstrate both to prove parity.
|
||||
|
||||
## 10. Loading states
|
||||
|
||||
Specified in MOTION_GUIDELINES §4; visually: skeleton = `surface-2` rounded block, aspect-locked, mono caption `loading capture…` bottom-left, no shimmer sweep (shimmer reads as ad-tech; static + cross-fade is calmer). Form submit dots and language cross-fade per A7/A9.
|
||||
|
||||
## 11. Language switch UX (M5 detail)
|
||||
|
||||
- Segmented `EN | NO` control, header right cluster, thumb slide 160ms.
|
||||
- Switching maps current page → same page other locale (URL strategy per IA §5); content cross-fade per A7; scroll ratio preserved.
|
||||
- First-visit hint (browser prefers nb): dismissible one-line banner under header: `Denne siden finnes på norsk → [Bytt] [×]` — shown once, never auto-redirect.
|
||||
- All chrome strings localise with content (nav labels, CV button, form labels, alt text, skip-link).
|
||||
|
||||
## 12. Case-study page (M3)
|
||||
|
||||
```
|
||||
┌ header ────────────────────────────────────────────────────────────────┐
|
||||
│ mono-label: CASE STUDY · PERSONAL PRODUCT │
|
||||
│ h1: JobTrack [ACTIVE ●] │
|
||||
│ body-lg: A job-application workspace covering the full journey │
|
||||
│ from application to follow-up. │
|
||||
│ chips: .NET 9 · React · TypeScript · SQLite · FastAPI · Ollama · │
|
||||
│ Docker links: [Repository ↗] │
|
||||
├─ hero screenshot (browser frame, full-bleed to 1200) ─────────────────┤
|
||||
├─ TL;DR box (surface-2, 3px accent left edge): What/Why/Stack/Role ────┤
|
||||
│ ┌ mini-TOC (sticky) ──┐ │
|
||||
│ ## Problem & context (72ch) │ Problem │ │
|
||||
│ ## Architecture │ Architecture ● │ │
|
||||
│ [diagram breakout 1200px] │ Decisions │ │
|
||||
│ ## Key decisions & trade-offs │ Security │ │
|
||||
│ numbered blocks: choice/alt/why │ Outcome │ │
|
||||
│ ## Security & production notes │ Next │ │
|
||||
│ ## Screenshots (lightbox gallery) └─────────────────────┘ │
|
||||
│ ## What I'd improve next │
|
||||
├─ footer nav: ← InboxIntel | All projects ─────────────────────────────┤
|
||||
```
|
||||
Diagram style per COLOUR_SYSTEM §3.5: surface nodes, accent flow, amber external systems (Gmail API), mono labels.
|
||||
|
||||
## 13. Asset integration notes
|
||||
|
||||
- **Portraits:** outdoor headshot → hero/about; square crop → favicon-adjacent/OG fallback; suit photo → not used on site (kept for LinkedIn/CV).
|
||||
- **JobTrack media plan** (from `website_details.md`): applications table, job-details dialog, Gmail import view, dashboard analytics — 4 captures + 1 architecture diagram + workflow diagram.
|
||||
- **InboxIntel media plan:** dashboard grid, cleanup preview/confirm flow, architecture diagram (Clean Architecture layers), sync-worker sequence sketch.
|
||||
- Screenshots need a capture pass at 2× on seeded demo data — **flagged as a Phase 3 prerequisite task** (no real personal job-hunt data on the public site; seed fictional companies).
|
||||
@@ -0,0 +1,38 @@
|
||||
# Portfolio Redesign — Phase 1 Deliverables (Design, no code)
|
||||
|
||||
Status: **complete, awaiting approval** · July 2026
|
||||
|
||||
## Index
|
||||
|
||||
### 01 — Research & strategy
|
||||
- [DESIGN_RESEARCH.md](01-research/DESIGN_RESEARCH.md) — audit of cesnimda.co.uk, portfolio/SaaS pattern research, strategic conclusions
|
||||
- [USER_PERSONAS.md](01-research/USER_PERSONAS.md) — recruiter, eng manager, Norwegian CTO, interviewer
|
||||
- [RECRUITER_BEHAVIOUR_ANALYSIS.md](01-research/RECRUITER_BEHAVIOUR_ANALYSIS.md) — scan behaviour, two-lane rule, Norway specifics, acceptance criteria
|
||||
|
||||
### 02 — UX structure
|
||||
- [INFORMATION_ARCHITECTURE.md](02-ux/INFORMATION_ARCHITECTURE.md) — site map, nav model, bilingual URL strategy, content model
|
||||
- [USER_JOURNEYS.md](02-ux/USER_JOURNEYS.md) — five validated journeys + requirements checklist
|
||||
- [CONTENT_STRATEGY.md](02-ux/CONTENT_STRATEGY.md) — voice, hero copy drafts (EN/NO), case-study angles, CV improvement plan
|
||||
- [CANONICAL_CONTENT.md](02-ux/CANONICAL_CONTENT.md) — master CV facts (provided by Connor, July 2026); single source for all copy
|
||||
|
||||
### 03 — Visual design system ("Kontrollrom")
|
||||
- [DESIGN_SYSTEM.md](03-visual/DESIGN_SYSTEM.md) — direction, identity, surfaces, components
|
||||
- [TYPOGRAPHY_GUIDE.md](03-visual/TYPOGRAPHY_GUIDE.md) · [COLOUR_SYSTEM.md](03-visual/COLOUR_SYSTEM.md) · [SPACING_SYSTEM.md](03-visual/SPACING_SYSTEM.md) · [MOTION_GUIDELINES.md](03-visual/MOTION_GUIDELINES.md)
|
||||
|
||||
### 04 — Interaction design
|
||||
- [ANIMATION_CONCEPTS.md](04-interaction/ANIMATION_CONCEPTS.md) — A1–A10 named concepts + rejected list
|
||||
- [MICRO_INTERACTIONS.md](04-interaction/MICRO_INTERACTIONS.md) — per-component states & keyboard behaviour
|
||||
- [SCROLL_EXPERIENCE.md](04-interaction/SCROLL_EXPERIENCE.md) · [CURSOR_INTERACTIONS.md](04-interaction/CURSOR_INTERACTIONS.md)
|
||||
|
||||
### 05 — Mockups
|
||||
- [MOCKUPS.md](05-mockups/MOCKUPS.md) — master spec covering all 11 required mockup scopes
|
||||
- Boards: M1 home desktop dark · M2 home desktop light (NO locale) · M3 case study · M4 mobile + nav overlay · M5 header/nav/control states · M6 projects/timeline/contact
|
||||
|
||||
## Key decisions requiring sign-off before Phase 2
|
||||
1. **Positioning:** "Systems Developer, 8+ years" — no "senior" self-label; seniority shown via evidence (CONTENT_STRATEGY §1)
|
||||
2. **IA:** multi-page (home + case studies + about/experience/contact/cv) replacing the one-pager (IA §1)
|
||||
3. **Bilingual URLs:** EN at root, NO under `/no/` with localised slugs; switch maps page↔page; no auto-redirect (IA §5)
|
||||
4. **Design direction:** "Kontrollrom" — dark-first, aurora-teal single accent, mono metadata, trace motif (DESIGN_SYSTEM)
|
||||
5. **InboxIntel framed as `IN DEVELOPMENT`** (honest status) (CONTENT_STRATEGY §4)
|
||||
6. **CV rework:** ATS-safe single-column variant becomes the download default (CONTENT_STRATEGY §6)
|
||||
7. **Screenshot capture pass on seeded demo data** = Phase 3 prerequisite (MOCKUPS §13)
|
||||
@@ -0,0 +1,38 @@
|
||||
# ANIMATION_SPEC.md
|
||||
Technical realisation of the Phase 1 motion system (MOTION_GUIDELINES + ANIMATION_CONCEPTS A1–A10). Library decision: **none** (TECH_SPEC §4) — CSS-first + the shared observer module.
|
||||
|
||||
---
|
||||
|
||||
## 1. Token implementation
|
||||
|
||||
Motion tokens are CSS custom properties (`--dur-instant: 80ms` … `--dur-signature: 700ms`, `--ease-out`, `--ease-in-out`) defined once in `motion.css`; every transition/keyframe references tokens. A single `@media (prefers-reduced-motion: reduce)` layer overrides: durations → 1ms (not 0 — ensures transitionend semantics still fire), translations → none, trace/pulse keyframes → final frame. No per-component reduced-motion logic anywhere.
|
||||
|
||||
## 2. Concept → mechanism map
|
||||
|
||||
| Concept (Phase 1) | Mechanism | Trigger |
|
||||
|---|---|---|
|
||||
| A1 Trace draw | Inline SVG, `stroke-dasharray/dashoffset` keyframe, dot `opacity` blink ×1 | CSS animation on load; `sessionStorage` flag adds `data-seen` on repeat visits → static |
|
||||
| A2 Section arrive | `[data-reveal]` class toggle → `opacity` + `translateY(12px)` transition; children staggered via `--reveal-index` custom property delay | `observer.ts`, threshold 0.2, once |
|
||||
| A3 Card focus | Pure CSS `:hover`/`:focus-visible` (border token, transform 4px, underline scale, arrow translate) | none (CSS) |
|
||||
| A4 Chip pulse | CSS keyframe ×2 iterations on `.status-active .dot`, started when `[data-reveal]` fires (animation-play-state pattern) | observer |
|
||||
| A5 Diagram flow | SVG with class-annotated nodes/edges; `:hover` on node → CSS sibling/descendant rules highlight edges, dim others via `opacity` | CSS only, `@media (hover: hover)` gate |
|
||||
| A6 Theme morph | Two-path SVG icon crossfade/morph via CSS; global colour transition on `data-theme` swap limited to a curated property list (avoid transitioning *everything*) | `theme.ts` |
|
||||
| A7 Language cross-fade | View Transitions API (same-document for the switch control; cross-document for navigation) with CSS-only fallback = normal navigation | browser-native, progressive |
|
||||
| A8 Timeline grow | Spine is a scaleY-transformed pseudo-element; dots opacity-staggered | observer |
|
||||
| A9 Form confirm | Panel swap with height-locked container; check icon `stroke-dashoffset` draw | `form.ts` on 2xx |
|
||||
| A10 404 fray | Static SVG with a short CSS keyframe (dash gap growth); reduced-motion static | load |
|
||||
| Header compress | `data-scrolled` boolean flag at >120px (observer sentinel element) → CSS height/blur transition. *Deviation from Phase 1:* linear scroll-mapping downgraded to a threshold toggle with smooth transition — visually near-identical, avoids scroll-linked JS entirely; scroll-driven CSS animations may restore true mapping as progressive enhancement where supported | observer |
|
||||
|
||||
## 3. Performance rules (enforceable)
|
||||
|
||||
- Animatable properties allow-list: `transform`, `opacity`, `stroke-dashoffset`, plus theme-swap colour transitions. Anything else fails review.
|
||||
- No `will-change` except trace + lightbox transitions (added/removed around use).
|
||||
- Observer instances: exactly one shared IntersectionObserver for reveals/spy + one sentinel for header. No scroll event listeners anywhere.
|
||||
- Lighthouse TBT budget (<100 ms) is the regression tripwire for animation JS.
|
||||
|
||||
## 4. Accessibility rules
|
||||
|
||||
- Reduced-motion layer per §1 — verified in Playwright with emulated `prefers-reduced-motion`.
|
||||
- Nothing flashes >3×/s (chip pulse is 2 iterations, ~1 Hz). No parallax beyond 4px.
|
||||
- Focus is never moved by animation; reveals never contain focusable elements while hidden (visibility handled so tab order is stable pre-reveal).
|
||||
- View-transition language swap must not discard focus position: switch control retains focus after navigation (tested).
|
||||
@@ -0,0 +1,105 @@
|
||||
# ARCHITECTURE.md
|
||||
System architecture for the bilingual portfolio. Companion detail specs: ROUTING_SPEC, COMPONENT_ARCHITECTURE, I18N_SPEC, DATA_MODEL, DOCKER_SPEC, DEPLOYMENT_SPEC.
|
||||
|
||||
---
|
||||
|
||||
## 1. System overview
|
||||
|
||||
```
|
||||
┌──────────────────────────── Connor's server ───────────────────────────┐
|
||||
Browser ── HTTPS ──► │ nginx (existing reverse proxy, TLS) │
|
||||
│ ├── cesnimda.co.uk/ → [site] nginx container (static) │
|
||||
│ ├── cesnimda.co.uk/api/contact → [contact-relay] .NET 9 container │
|
||||
│ └── git.cesnimda.uk → Gitea (existing) │
|
||||
│ │
|
||||
│ docker compose stack "resumesite": site + contact-relay (+ optional │
|
||||
│ analytics slot) │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Build time (CI, Gitea Actions):
|
||||
content (markdown/JSON, images) ─► Astro build ─► static dist/ ─► site image (nginx + dist)
|
||||
└► budgets/tests gate the publish
|
||||
```
|
||||
|
||||
**Architectural style: static-first with one stateless dynamic sidecar.** All pages are pre-rendered at build time (SSG). There is no runtime rendering, no database, no sessions. The only mutable runtime path is `POST /api/contact`, isolated in its own container so the site itself has zero attack surface beyond static file serving.
|
||||
|
||||
## 2. Repository layout (single repo: `ResumeSite` on git.cesnimda.uk)
|
||||
|
||||
```
|
||||
ResumeSite/
|
||||
├── site/ # Astro project
|
||||
│ ├── src/
|
||||
│ │ ├── pages/ # route files (EN root) — see ROUTING_SPEC
|
||||
│ │ │ └── no/ # Norwegian route tree (localised slugs)
|
||||
│ │ ├── layouts/ # Base, Page, CaseStudy layouts
|
||||
│ │ ├── components/ # see COMPONENT_ARCHITECTURE
|
||||
│ │ │ ├── core/ # Header, Footer, Seo, ThemeScript, SkipLink
|
||||
│ │ │ ├── home/ # Hero, ProofStrip, SkillsGrid, ProjectCards, Timeline, ContactBand
|
||||
│ │ │ ├── case-study/ # TldrBox, MiniToc, DecisionList, Diagram, Gallery
|
||||
│ │ │ └── ui/ # Chip, Button, SplitButton, LangSwitch, ThemeToggle, Card
|
||||
│ │ ├── content/ # content collections (the bilingual data layer)
|
||||
│ │ │ ├── projects/{en,no}/...
|
||||
│ │ │ ├── experience/{en,no}/...
|
||||
│ │ │ └── profile/{en,no}.json
|
||||
│ │ ├── i18n/ # locales.ts, dictionary.{en,no}.ts, slugMap.ts, t() helper
|
||||
│ │ ├── styles/ # tokens.css (design tokens), base.css, motion.css
|
||||
│ │ ├── scripts/ # island helpers: observer.ts, lightbox.ts, nav.ts, form.ts, theme.ts
|
||||
│ │ └── assets/ # source images, diagrams (SVG), fonts
|
||||
│ ├── public/ # CVs (pdf), favicon, robots.txt
|
||||
│ └── (astro/tailwind/ts configs)
|
||||
├── relay/ # .NET 9 minimal API contact relay (own Dockerfile)
|
||||
├── deploy/ # compose files, nginx site config, env templates
|
||||
├── .gitea/workflows/ # CI/CD pipelines
|
||||
├── docs/ # this spec set + phase-1 design (moved into repo at Phase 3 start)
|
||||
└── tests/ # playwright e2e (vitest lives beside site/src)
|
||||
```
|
||||
|
||||
Rationale: one repo keeps content, design docs, infra and both services versioned together — appropriate for a single-owner product and makes the repo itself portfolio evidence.
|
||||
|
||||
## 3. Layered architecture of the site
|
||||
|
||||
```
|
||||
┌ Content layer markdown + JSON content collections, schema-validated (zod) — DATA_MODEL.md
|
||||
├ i18n layer locale config, slug map, typed dictionaries — I18N_SPEC.md
|
||||
├ Template layer layouts + components consume {content, locale} → HTML — COMPONENT_ARCHITECTURE.md
|
||||
├ Token layer CSS custom properties (themes) + Tailwind utilities
|
||||
├ Behaviour layer 5 small TS modules (theme, nav, observer, lightbox, form) as progressive enhancement
|
||||
└ Delivery layer static dist/ in nginx container; immutable-hashed assets; long-cache headers
|
||||
```
|
||||
|
||||
Dependency rule (enforced by review, mirrors the projects' clean-architecture story): **templates depend on content+i18n; content depends on nothing; behaviour modules depend on DOM contracts (data-attributes), never on content.** No component reads raw filesystem or hardcodes strings.
|
||||
|
||||
## 4. Key architectural decisions (ADR summary)
|
||||
|
||||
| # | Decision | Alternative rejected | Why |
|
||||
|---|---|---|---|
|
||||
| A1 | Full SSG, no SSR | Astro SSR/hybrid | No per-request data exists; SSR adds a Node runtime, cold-start surface, and ops burden for zero benefit |
|
||||
| A2 | Contact relay as separate container | SSR endpoint in site / SaaS form | Keeps site image immutable+static; isolates the only untrusted-input path; self-host principle |
|
||||
| A3 | Content collections in-repo | Headless CMS (Directus/Strapi/…) | Single author, git workflow is his CMS; removes a service, a database, and an attack surface |
|
||||
| A4 | CSS custom-property tokens, `data-theme` attribute switch | Tailwind `dark:` variants only | One token source drives both themes + allows no-flash inline theme script + inspectable tokens |
|
||||
| A5 | Localised slugs via explicit map module | File-name convention magic | The EN↔NO page mapping is a *contract* used by header switch, hreflang, sitemap — must be a single typed artifact testable in CI |
|
||||
| A6 | Behaviour as data-attribute modules | Framework islands | JS budget < 40 kB; three behaviours don't justify a runtime; escape hatch to Preact pre-authorised if one outgrows it |
|
||||
| A7 | Images processed at build | Runtime image service | Static hosting; fixed dimensions guarantee CLS 0; AVIF/WebP generated once |
|
||||
| A8 | CVs as versioned static assets in `public/` with stable URLs (`/cv/connor-babbington-cv-en.pdf`) | External drive links | Stable recruiter-shareable URLs; git-versioned; locale-matched download logic trivial |
|
||||
|
||||
## 5. Performance strategy (architecture-level)
|
||||
|
||||
- **Budget ownership:** budgets live in CI (TECH_SPEC §9); architecture guarantees they're achievable: no runtime framework, fonts subset + preloaded, hero has no image dependency for LCP (LCP element = H1 text), screenshots below fold lazy + aspect-locked.
|
||||
- **Caching:** hashed asset filenames → `Cache-Control: immutable, 1y`; HTML `no-cache` (revalidate) so deploys are instant; CV PDFs short cache (they update).
|
||||
- **Critical path:** inline theme-init script (tiny, blocking by design to prevent theme flash) + single CSS file per page; JS deferred entirely.
|
||||
- **Third-party requests: zero.** No CDN fonts, no analytics beacons (unless self-hosted later), no embeds. This is both performance and the GDPR/no-cookie-banner stance.
|
||||
|
||||
## 6. Security posture
|
||||
|
||||
- Static site: attack surface = nginx serving files. Headers set at the site-nginx layer: CSP (no inline scripts except the hashed theme script, no external origins), HSTS (at the edge proxy), X-Content-Type-Options, Referrer-Policy, Permissions-Policy minimal.
|
||||
- Relay: input validation + size caps, honeypot + minimum-fill-time trap, per-IP rate limit, SMTP credentials via env/secret (never in image), no persistence, structured minimal logging (no message bodies), runs as non-root, read-only filesystem.
|
||||
- Supply chain: pnpm lockfile committed, Renovate (already familiar from InboxIntel) on the repo, images pinned by digest in compose.
|
||||
|
||||
## 7. Failure modes & degradation
|
||||
|
||||
| Failure | Behaviour |
|
||||
|---|---|
|
||||
| Relay down | Form shows the fallback panel (direct e-mail + copy button) — conversion path survives |
|
||||
| JS disabled/broken | Full content readable; nav works (plain links); no lightbox/reveals — by design (progressive enhancement) |
|
||||
| Old cached HTML after deploy | HTML revalidates; hashed assets never mismatch |
|
||||
| Locale content missing (build-time) | CI fails the build (parity test) — never ships a broken language |
|
||||
@@ -0,0 +1,69 @@
|
||||
# COMPONENT_ARCHITECTURE.md
|
||||
|
||||
## 1. Principles
|
||||
|
||||
1. **Server components by default** (Astro components, zero client JS). Client behaviour only via the five behaviour modules bound by `data-*` attributes.
|
||||
2. **Components receive typed props from content + locale; they never fetch or read files.** Layouts do the data plumbing.
|
||||
3. **One component, both themes, both locales** — no `HeroNo.astro`, no `CardDark.astro`. Locale comes in as a prop bundle; theme is pure CSS.
|
||||
4. Naming and states follow the Phase 1 component inventory (DESIGN_SYSTEM §5) 1:1 — design system and component tree share vocabulary.
|
||||
|
||||
## 2. Component tree
|
||||
|
||||
```
|
||||
layouts/
|
||||
Base html/head/meta, Seo, ThemeScript(inline), fonts preload, SkipLink,
|
||||
Header, <slot/>, Footer, behaviour-module script tags (deferred)
|
||||
Page Base + page-title block + prose container (about/contact/cv/colophon)
|
||||
Home Base + section scaffolding with reveal data-attributes
|
||||
CaseStudy Base + case-study scaffold (header block, MiniToc, section renderer)
|
||||
|
||||
components/core/
|
||||
Header nav links (slug-map-driven), LangSwitch, ThemeToggle, SplitCvButton;
|
||||
states: default/scrolled via data-scrolled (observer module)
|
||||
MobileNav overlay panel (template in DOM, hidden; nav.ts toggles, traps focus)
|
||||
Footer channels, CV links, language repeat, colophon link
|
||||
Seo per-page meta, hreflang pairs (slug map), OG, JSON-LD builders
|
||||
ThemeScript inline no-flash theme init (the one permitted inline script, CSP-hashed)
|
||||
SkipLink first focusable element
|
||||
|
||||
components/ui/ (design-system atoms — dumb, prop-driven)
|
||||
Chip(kind: fact|status|stack, status?) Button(variant: primary|secondary|ghost)
|
||||
SplitCvButton(locale, cvMeta) LangSwitch(currentPageId, locale)
|
||||
ThemeToggle Card(interactive?)
|
||||
SectionLabel(number, text) TraceMotif(animated?)
|
||||
FramedImage(media, loading) Tooltip(delay 600ms)
|
||||
|
||||
components/home/
|
||||
Hero(profile) ProofStrip(4 tiles from profile/skills data)
|
||||
SkillsGrid(skillGroups) ProjectCards(featured projects, homelab slim card)
|
||||
ExperienceTimeline(entries) AboutTeaser(profile) ContactBand(profile)
|
||||
|
||||
components/case-study/
|
||||
CsHeader(project) TldrBox(project.tldr)
|
||||
MiniToc(sections, sticky ≥1200) SectionRenderer(section) — markdown → prose
|
||||
DecisionList(decisions) ArchDiagram(diagram, locale labels)
|
||||
Gallery(media) + Lightbox host NextPrev(project order)
|
||||
```
|
||||
|
||||
## 3. Behaviour modules (the entire client JS surface)
|
||||
|
||||
| Module | Binds to | Responsibility | Budget |
|
||||
|---|---|---|---|
|
||||
| `theme.ts` | ThemeToggle + inline init | read/persist preference, `data-theme` swap, icon morph class | ~1 kB |
|
||||
| `nav.ts` | Header/MobileNav | overlay open/close, focus trap, scroll lock, Esc; header `data-scrolled` flag | ~2 kB |
|
||||
| `observer.ts` | `[data-reveal]`, `[data-spy]` | shared IntersectionObserver: reveal-once classes, scroll-spy for nav/MiniToc, timeline draw trigger | ~2 kB |
|
||||
| `lightbox.ts` | Gallery | open/close/navigate, focus trap, key handling, swipe | ~3 kB |
|
||||
| `form.ts` | Contact form | client validation, honeypot timing, POST to relay, A9 success/error panels, copy-to-clipboard buttons | ~3 kB |
|
||||
|
||||
Rules: modules are independent (no shared state beyond `localStorage` keys), fail-silent (feature simply absent if errored), loaded `defer`, total ≤ 15 kB gzip — well under the 40 kB budget with margin for the language-hint logic (~1 kB, lives in `nav.ts`).
|
||||
|
||||
## 4. Contracts between design tokens and components
|
||||
|
||||
- Components use Tailwind utilities mapped to tokens; **no raw hex/px in component code** (lint-guarded where feasible).
|
||||
- State styling via data-attributes (`data-scrolled`, `data-open`, `data-revealed`, `data-status="active"`) so behaviour modules never touch inline styles — CSS owns all appearance.
|
||||
- Motion classes come exclusively from `motion.css` (token durations + reduced-motion layer) — a component never declares its own transition values.
|
||||
|
||||
## 5. Reuse & variant policy
|
||||
|
||||
- Case-study template is one component set; JobTrack vs InboxIntel differ only in content. Homelab's `capability` template reuses CsHeader/SectionRenderer, omits TldrBox/MiniToc/DecisionList.
|
||||
- The 404, hint bar, and colophon reuse existing atoms only — no bespoke components for one-offs unless an atom can't express it (guard against component sprawl in a one-person codebase).
|
||||
@@ -0,0 +1,76 @@
|
||||
# DATA_MODEL.md
|
||||
Content model for the bilingual content collections. All schemas zod-validated at build; a schema violation fails CI. (Conceptual field lists — not code.)
|
||||
|
||||
---
|
||||
|
||||
## 1. Collections overview
|
||||
|
||||
```
|
||||
content/
|
||||
├── profile/ singleton per locale (hero, chips, about)
|
||||
├── projects/ 3 entries × 2 locales (jobtrack, inboxintel, homelab)
|
||||
├── experience/ 5 entries × 2 locales (council, side roles, education, 2023-now)
|
||||
├── skills/ 3 group entries × 2 locales
|
||||
└── meta/ colophon page, 404 copy, contact page copy × 2 locales
|
||||
```
|
||||
|
||||
Locale-invariant fields live on the EN entry and merge into NO at load (I18N_SPEC §3). Below, fields marked **[inv]** are invariant; everything else is per-locale prose.
|
||||
|
||||
## 2. `profile` (singleton)
|
||||
|
||||
- `name` [inv], `roleLine`, `heroTagline` (the one-sentence claim), `heroSummary` (≤ 2 lines)
|
||||
- `chips[]`: `{ id [inv], label }` — location, permit, availability, languages
|
||||
- `statusLine` (trace-dot tooltip: "open to work" / "åpen for muligheter")
|
||||
- `aboutParagraphs[]` (3), `interestsLine`
|
||||
- `photo` [inv]: asset ref + per-locale `alt`, `caption`
|
||||
- `links` [inv]: email, linkedinRedirect (`/Linkedin`), gitea, phone
|
||||
- `cv` [inv]: `{ en: {path, sizeKb, updated}, no: {...} }`
|
||||
|
||||
## 3. `projects`
|
||||
|
||||
- `id`/`slug` [inv, per-locale slug via slug map], `name` [inv], `order` [inv]
|
||||
- `status` [inv]: `active | in-development | archived` (drives chip semantics per COLOUR_SYSTEM §3.4)
|
||||
- `valueProp` (one-liner), `cardTeaser` (2 lines for homepage card)
|
||||
- `stack[]` [inv]: `{ name, contextLine? per-locale }` (chip + tooltip)
|
||||
- `links[]` [inv]: `{ type: repo|live|docs, url, public: bool }` (repo links only if/when repos are public — publish decision is content, not code)
|
||||
- `tldr`: `{ what, why, stack, role }` (4 strings)
|
||||
- `sections[]`: ordered `{ anchorId per-locale, heading, body (markdown) }` — canonical section kinds: `problem`, `architecture`, `decisions`, `security`, `screenshots`, `next`
|
||||
- `decisions[]`: structured `{ n, choice, alternative, rationale }` (rendered as the numbered trade-off blocks)
|
||||
- `diagram` [inv]: SVG asset ref + per-locale `title`, `desc` (a11y), `nodes[]` label overrides per locale
|
||||
- `media[]` [inv refs]: `{ image, width, height, per-locale alt + caption, order }` — dimensions mandatory (CLS 0)
|
||||
- `template` [inv]: `case-study | capability` (homelab uses the lighter template)
|
||||
- `summaryOnly?` (NO-side flag per I18N_SPEC §4.2)
|
||||
|
||||
## 4. `experience`
|
||||
|
||||
- `id` [inv], `employer` [inv], `location` [inv], `period` [inv]: `{ from, to|null }`
|
||||
- `role`, `summary`, `highlights[]` (featured only)
|
||||
- `emphasis` [inv]: `featured | compact`
|
||||
- `progression?` [inv]: for the council entry — `[{from: 2015, to: 2017, label-per-locale: "Apprentice"}, {from: 2017, to: 2023, label: "System Developer"}]`
|
||||
- `anchor?` — the confirmed concrete bullet (streetlight/highways system) is content, phrased per CANONICAL_CONTENT
|
||||
- Entries: `warwickshire` (featured), `royal-vapes`, `hodcarrier`, `nuffield` (compact, `alongside: true` [inv] flag renders the "alongside" grouping), `education`, `norway-now` (2023–, norskkurs + products)
|
||||
|
||||
## 5. `skills`
|
||||
|
||||
- `id` [inv], `order` [inv], `title`, `contextLine`, `skills[]`: `{ name [inv], tooltip? }`
|
||||
- Groups: `development`, `devops-infrastructure`, `practices`
|
||||
|
||||
## 6. Page-meta (every routable page)
|
||||
|
||||
- `title`, `description` (≤155 chars), `ogImage` [generated per locale], `pageId` [inv → slug map join]
|
||||
- Case studies additionally emit structured-data fields (SEO_SPEC §3)
|
||||
|
||||
## 7. Asset conventions
|
||||
|
||||
- Screenshots: `assets/projects/<id>/<capture>.png` source of truth at 2×, seeded demo data only (Phase 3 prerequisite); pipeline emits AVIF/WebP/fallback + srcset.
|
||||
- Diagrams: hand-authored SVGs following the design-system diagram style; text within diagrams uses per-locale label injection at build (single SVG source, two rendered outputs) — avoids maintaining twin diagrams.
|
||||
- Fonts/CVs/favicons in `public/` (unprocessed, stable URLs).
|
||||
|
||||
## 8. Validation rules (CI)
|
||||
|
||||
1. Locale parity (I18N_SPEC §4.1).
|
||||
2. Every `media[]` entry has dimensions + both alts.
|
||||
3. Every project has ≥3 `decisions[]` and a `security` section (the site's promise to P2 is structural).
|
||||
4. `status` transitions logged in git only — no "archived" entry may appear on the homepage cards (query rule).
|
||||
5. Description lengths, title lengths enforced (SEO).
|
||||
6. All external `links[].url` respond 200 in a scheduled link-check workflow (not per-commit).
|
||||
@@ -0,0 +1,53 @@
|
||||
# DEPLOYMENT_SPEC.md
|
||||
|
||||
## 1. Environments
|
||||
|
||||
| Env | Where | Purpose | URL |
|
||||
|---|---|---|---|
|
||||
| **dev** | Local (Docker compose dev stack) | Inner loop, HMR | localhost:4321 |
|
||||
| **staging** | Same server, separate compose project | Pre-release verification on real infra (headers, proxy, relay SMTP against a mailbox test) | `staging.cesnimda.co.uk` (basic-auth gated at the proxy, `noindex` everywhere) |
|
||||
| **prod** | Server, compose stack `resumesite` | Live | `cesnimda.co.uk` |
|
||||
|
||||
Staging exists chiefly for the **cutover** and for occasional risky changes; day-to-day content edits may go dev → prod (single-owner pragmatism, gates make it safe).
|
||||
|
||||
## 2. Pipeline (Gitea Actions, all self-hosted)
|
||||
|
||||
```
|
||||
push / PR to main
|
||||
├─ lint + typecheck + astro check
|
||||
├─ unit (vitest: i18n parity, slug-map bijection, JSON-LD builders)
|
||||
├─ build site (static) ──► Playwright e2e vs built output + relay container
|
||||
│ ──► axe a11y scan (both locales, both themes)
|
||||
│ ──► Lighthouse CI budgets (fail = red)
|
||||
├─ build + push images :sha and :latest → Gitea registry (main only)
|
||||
└─ deploy job (main only, manual approval toggle):
|
||||
ssh to server → compose pull → compose up -d → smoke check
|
||||
(curl /, /no/, /api/contact healthz, spot-check headers) → notify
|
||||
```
|
||||
|
||||
- **Rollback:** redeploy previous `:sha` tag (one command, documented runbook); HTML no-cache means rollback is immediate. Registry retains last N images.
|
||||
- **Content-only changes** ride the same pipeline — content errors are build errors by design (schema + parity tests), which is the safety that replaces CMS preview.
|
||||
- Scheduled workflow (weekly): external link check (DATA_MODEL §8.6) + Lighthouse drift report + Renovate PRs.
|
||||
|
||||
## 3. Cutover plan (WordPress → new site)
|
||||
|
||||
1. Pre-launch: staging fully verified (both locales, CV downloads, form → real mailbox, redirects incl. `/Linkedin`, 410 map, OG previews via LinkedIn/Slack debuggers, Search Console fetch).
|
||||
2. URL inventory + redirect/410 map finalised (SEO_SPEC §6).
|
||||
3. Cutover = repoint host nginx server-block from WP to the `site` container (single reload; WP left running dark for 2 weeks as instant rollback).
|
||||
4. Post-launch checklist: submit sitemaps, watch Search Console coverage + relay logs for form spam patterns, then decommission WP + its PHP/MySQL surface (attack-surface win worth noting on the colophon).
|
||||
|
||||
## 4. Operational guardrails
|
||||
|
||||
- **Uptime:** monitored by existing homelab monitoring (add checks: `/` 200 + content match, `/no/` 200, relay healthz); alerting via his current channel.
|
||||
- **TLS:** unchanged at the existing edge proxy (already terminating for Gitea etc.).
|
||||
- **Logs:** site-nginx access logs minimal (no query bodies, standard rotation); relay logs metadata only (timestamp, IP hash, outcome) — privacy stance documented on colophon.
|
||||
- **CV updates:** replace PDF in repo → pipeline redeploys; stable URLs mean previously shared links always fetch the newest CV — an explicit product decision.
|
||||
- **Secrets rotation:** SMTP credential rotation runbook (env file update + `compose up -d relay`), noted in deploy README.
|
||||
|
||||
## 5. Definition of Done for Phase 3 (deployment perspective)
|
||||
|
||||
1. All CI gates green on main; images in registry.
|
||||
2. Staging sign-off checklist complete (incl. native-Norwegian content review — I18N_SPEC §6).
|
||||
3. Cutover executed; WP dark-standby; monitoring green 48h.
|
||||
4. Search Console: sitemaps accepted, no coverage regressions after 4 weeks (watch item, not blocker).
|
||||
5. Colophon page published describing the stack honestly (the P2 hook).
|
||||
@@ -0,0 +1,60 @@
|
||||
# DOCKER_SPEC.md
|
||||
Container strategy. Descriptive spec — actual Dockerfiles/compose files are Phase 3 artifacts.
|
||||
|
||||
---
|
||||
|
||||
## 1. Images
|
||||
|
||||
### `site` (production)
|
||||
- **Multi-stage build:** stage 1 `node:22-alpine` + pnpm → install (frozen lockfile) → `astro build` → `dist/`; stage 2 `nginx:alpine` (digest-pinned) copying `dist/` + a site-local nginx config.
|
||||
- Site-local nginx config responsibilities: gzip/brotli precompressed asset serving, cache headers (immutable hashed assets / no-cache HTML per ARCHITECTURE §5), security headers + CSP (ARCHITECTURE §6), trailing-slash 301s, `/Linkedin` 301, legacy-WP 410 map, custom 404 page wiring.
|
||||
- Runs as non-root (nginx unprivileged image), read-only filesystem, no writable volumes, port 8080 internal.
|
||||
- Image contains **zero secrets and zero build tooling** — final image is nginx + static files, ~25 MB.
|
||||
|
||||
### `contact-relay` (production)
|
||||
- Multi-stage: .NET 9 SDK build → `mcr` aspnet runtime-deps/alpine final; non-root, read-only FS, listens 8081 internal.
|
||||
- Config via environment only: `Smtp__Host/Port/User/Password (secret)`, `Relay__ToAddress`, `Relay__RateLimit*`, `Relay__AllowedOrigin` (site origin for CORS/`Origin` check).
|
||||
- Healthcheck endpoint `/healthz` (no auth, no info leakage) wired to compose healthcheck.
|
||||
|
||||
### `site-dev` (development)
|
||||
- `node:22` dev container running `astro dev` with HMR; `site/` bind-mounted, `node_modules` in a named volume (Windows-host performance), port 4321 published.
|
||||
- Relay runs alongside via `dotnet watch` in an SDK-image dev container (or directly on host — both supported); Astro dev proxy forwards `/api/contact` → relay so the form works identically in dev.
|
||||
- Purpose: parity with prod behaviour (headers/proxying approximated) while keeping the fast inner loop; contributors need only Docker, not local Node/.NET installs.
|
||||
|
||||
## 2. Compose topology
|
||||
|
||||
```
|
||||
deploy/
|
||||
docker-compose.yml # prod: site + contact-relay, internal network,
|
||||
# exposed only to the host reverse-proxy network
|
||||
docker-compose.dev.yml # dev: site-dev + relay-dev
|
||||
.env.example # documented variables, no real values
|
||||
```
|
||||
|
||||
- Prod compose joins the server's existing reverse-proxy network; the host nginx (existing, TLS-terminating) routes `cesnimda.co.uk` → `site:8080` and `cesnimda.co.uk/api/contact` → `contact-relay:8081`. The stack publishes **no host ports** itself.
|
||||
- `restart: unless-stopped`, log rotation via compose logging options, images referenced by registry tag + digest.
|
||||
- Optional third service slot (`analytics`, self-hosted Umami/Plausible) reserved in the compose file as a commented profile — decision deferred.
|
||||
|
||||
## 3. Environment variables
|
||||
|
||||
| Var | Service | Notes |
|
||||
|---|---|---|
|
||||
| `SMTP_HOST/PORT/USER` | relay | His mail provider's submission endpoint |
|
||||
| `SMTP_PASSWORD` | relay | Secret — provided via env file with 600 perms on server (or Docker secret); never in git, never in image |
|
||||
| `RELAY_TO` | relay | Destination inbox |
|
||||
| `RELAY_ALLOWED_ORIGIN` | relay | `https://cesnimda.co.uk` |
|
||||
| `RELAY_RATE_*` | relay | Requests/window per IP |
|
||||
| `PUBLIC_SITE_URL` | site (build-time) | Canonical origin for sitemap/OG absolute URLs |
|
||||
|
||||
Build-time vs runtime separation is explicit: the static site consumes variables **only at build**; the relay **only at runtime**. `.env.example` documents every variable (pattern Connor already uses in both projects).
|
||||
|
||||
## 4. Build pipeline approach (interface to DEPLOYMENT_SPEC)
|
||||
|
||||
- CI builds both images on Gitea Actions runners, tags `:<git-sha>` + `:latest`, pushes to **Gitea's built-in container registry** (git.cesnimda.uk — keeps the whole chain self-hosted).
|
||||
- The `site` image build runs the full quality gate first (typecheck, unit, Playwright against a preview build, Lighthouse budgets) — an image only exists if gates passed.
|
||||
- Reproducibility: pnpm frozen lockfile, pinned base images, no network fetches during `astro build` beyond npm (fonts vendored in repo).
|
||||
|
||||
## 5. Local ops conventions
|
||||
|
||||
- `make`-style task runner or npm scripts wrapping compose invocations (`dev`, `build`, `test`, `deploy`) so every workflow is one documented command — README-driven, same convention as JobTrack/InboxIntel repos.
|
||||
- Backups: nothing stateful to back up (site is git + registry; relay is stateless) — the deliberate architectural win; documented so nobody adds state casually.
|
||||
@@ -0,0 +1,46 @@
|
||||
# I18N_SPEC.md
|
||||
|
||||
## 1. Architecture summary
|
||||
|
||||
Three i18n concerns, three mechanisms — never mixed:
|
||||
|
||||
| Concern | Mechanism | Examples |
|
||||
|---|---|---|
|
||||
| **Routes** | Astro built-in i18n (defaultLocale `en` at root, `no` prefixed) + slug map module | `/projects/jobtrack` ↔ `/no/prosjekter/jobtrack` |
|
||||
| **UI strings** (~60 keys) | Typed dictionary modules per locale + `t(key)` accessor, compile-time checked | `nav.projects`, `cv.download`, `form.error.email`, `a11y.skipLink` |
|
||||
| **Content** (prose, case studies, experience) | Content collections with per-locale entries sharing one zod schema | `projects/en/jobtrack.md` + `projects/no/jobtrack.md` |
|
||||
|
||||
No runtime i18n library. Locale is resolved from the URL at build time; every page is rendered once per locale into static HTML.
|
||||
|
||||
## 2. Dictionary spec
|
||||
|
||||
- Shape: nested typed object; both locale files implement the same interface → a missing key is a **compile error**, not a runtime fallback.
|
||||
- Keys are semantic (`cv.download`), never English-sentence keys.
|
||||
- Interpolation: simple `{param}` tokens only; no plural engine (the ~3 plural cases are written out per locale).
|
||||
- A11y strings are first-class dictionary entries: skip-link, lang-switch announcement ("Bytt til norsk" labelled *in the target language* per MICRO_INTERACTIONS), image alt patterns, form validation messages.
|
||||
|
||||
## 3. Content collection localisation
|
||||
|
||||
- Each collection folder splits `en/` and `no/`; entries share the same filename (`jobtrack.md`) = the join key.
|
||||
- One schema validates both locales (DATA_MODEL.md); locale-invariant fields (dates, stack chips, links, image refs, status) live in the EN entry and are **inherited** by the NO entry at load time (a small content-loader utility merges them) — this is the anti-duplication mechanism: facts exist once, prose exists twice.
|
||||
- Section anchors: schema field `anchorId` per section, per locale (`architecture`/`arkitektur`) with the pageId-level mapping used by the language switch to land on the equivalent section (J5).
|
||||
|
||||
## 4. Fallback rules (build-time, never runtime)
|
||||
|
||||
1. **Full parity is the shipping rule.** CI parity test: every EN content entry has a NO twin and vice versa; every dictionary key present in both.
|
||||
2. **Temporary asymmetry** (a new case study written EN-first): the NO twin must still exist, containing a native-Norwegian summary + link "Full versjon på engelsk" (CONTENT_STRATEGY rule). The schema supports `summaryOnly: true` for this state; CI allows it but the sitemap/hreflang still emit correctly.
|
||||
3. No silent English fallback ever renders on a `/no/` page — a Norwegian reader must never hit unexplained English mid-page. (Machine translation is banned outright.)
|
||||
|
||||
## 5. Locale-sensitive rendering rules
|
||||
|
||||
- `<html lang>`: `en-GB` / `nb-NO` per page. Date formatting: `2015–2023` style is locale-neutral; long dates use locale month names via `Intl` at build time.
|
||||
- Quotes: “ ” (EN) / « » (NO) — enforced in content review, not code.
|
||||
- The CV split-button defaults to the *current page locale's* CV; stored language preference overrides on EN pages only if the user previously chose NO (subtle: preference sets default, page locale is tiebreak).
|
||||
- OG/meta per locale (SEO_SPEC). `hreflang` emitted from the slug map: `en`, `nb`, `x-default → en`.
|
||||
|
||||
## 6. Translation management workflow
|
||||
|
||||
- Author EN and NO in the same PR (content files sit side by side; the parity test enforces it).
|
||||
- Norwegian quality gate before launch: native/fluent review pass of all NO content (tracked as a Phase 3 exit criterion; the reviewer reviews *markdown files in a PR*, which keeps review diffs clean — a deliberate benefit of content-in-git).
|
||||
- Glossary (CONTENT_STRATEGY §5.3) lives as a comment header in the NO dictionary file so it's in front of whoever edits translations.
|
||||
- No TMS tooling: two locales, one author, git history is the audit trail.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Portfolio Redesign — Phase 2 Deliverables (Architecture & Technical Specification, no code)
|
||||
|
||||
Status: **complete, awaiting approval** · July 2026 · Builds on approved Phase 1 (`../design/`)
|
||||
|
||||
## Index
|
||||
|
||||
- [TECH_SPEC.md](TECH_SPEC.md) — stack evaluation & recommendation (Astro 5 + Tailwind 4 tokens, CSS-first motion, no i18n library, .NET 9 contact relay, testing/quality gates)
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — system overview, repo layout, layering, ADR summary, performance/security posture, failure modes
|
||||
- [ROUTING_SPEC.md](ROUTING_SPEC.md) — canonical EN↔NO route table (slug map contract), redirects, legacy-WP handling, `/Linkedin` preservation
|
||||
- [I18N_SPEC.md](I18N_SPEC.md) — three-mechanism i18n (routes/dictionary/content), fallback rules, translation workflow
|
||||
- [DATA_MODEL.md](DATA_MODEL.md) — content collections, schemas, locale-invariant field inheritance, CI validation rules
|
||||
- [COMPONENT_ARCHITECTURE.md](COMPONENT_ARCHITECTURE.md) — component tree, 5 behaviour modules (≤15 kB total), token/data-attribute contracts
|
||||
- [ANIMATION_SPEC.md](ANIMATION_SPEC.md) — Phase 1 concepts A1–A10 mapped to CSS/observer mechanisms, reduced-motion layer, perf rules
|
||||
- [SEO_SPEC.md](SEO_SPEC.md) — metadata, hreflang from slug map, JSON-LD, build-time OG images, WP-migration protection
|
||||
- [DOCKER_SPEC.md](DOCKER_SPEC.md) — site (nginx static) + contact-relay images, dev containers, compose topology, env vars
|
||||
- [DEPLOYMENT_SPEC.md](DEPLOYMENT_SPEC.md) — Gitea Actions pipeline, staging, cutover plan from WordPress, rollback, DoD
|
||||
|
||||
## Headline decisions requiring sign-off before Phase 3
|
||||
|
||||
1. **Astro 5 (static output) over Next.js** — content site, zero runtime JS by default; React competence is proven by the case studies, judgement by this choice (TECH_SPEC §2)
|
||||
2. **No animation/i18n/state libraries** — CSS + 5 tiny TS modules, ≤15 kB JS total (TECH_SPEC §4–6)
|
||||
3. **Contact form = self-hosted .NET 9 relay container** with mailto/copy fallback (TECH_SPEC §7)
|
||||
4. **Everything self-hosted**: Gitea Actions CI, Gitea container registry, deploy to existing server behind existing reverse proxy (DOCKER/DEPLOYMENT)
|
||||
5. **Content lives in git** (schema-validated collections), no CMS (ARCHITECTURE A3)
|
||||
6. **Staging env + dark-standby WP cutover** with redirect/410 map and `/Linkedin` redirect preserved (DEPLOYMENT §3, ROUTING §2)
|
||||
|
||||
## Phase 3 prerequisites carried forward
|
||||
- Screenshot capture pass on seeded demo data (JobTrack, InboxIntel)
|
||||
- ATS-safe CV rework (both languages) — becomes the download default
|
||||
- Native-Norwegian review pass of all NO content (launch gate)
|
||||
- Legacy WordPress URL inventory for the redirect map
|
||||
@@ -0,0 +1,49 @@
|
||||
# ROUTING_SPEC.md
|
||||
|
||||
## 1. Canonical route table (the EN↔NO contract)
|
||||
|
||||
| Page | EN (default, root) | NO (`/no` prefix, localised slugs) |
|
||||
|---|---|---|
|
||||
| Home | `/` | `/no/` |
|
||||
| Projects index | `/projects/` | `/no/prosjekter/` |
|
||||
| JobTrack case study | `/projects/jobtrack/` | `/no/prosjekter/jobtrack/` |
|
||||
| InboxIntel case study | `/projects/inboxintel/` | `/no/prosjekter/inboxintel/` |
|
||||
| Homelab capability page | `/projects/homelab/` | `/no/prosjekter/hjemmelab/` |
|
||||
| Experience | `/experience/` | `/no/erfaring/` |
|
||||
| About | `/about/` | `/no/om-meg/` |
|
||||
| Contact | `/contact/` | `/no/kontakt/` |
|
||||
| CV hub | `/cv/` | `/no/cv/` |
|
||||
| Site meta ("how this site works") | `/colophon/` | `/no/kolofon/` |
|
||||
| 404 | `/404` (bilingual page) | — |
|
||||
|
||||
Rules: trailing-slash canonical (nginx redirects the bare form, 301). Product names (jobtrack, inboxintel) stay untranslated per CONTENT_STRATEGY glossary. This table lives in code as the single **slug map module** (`i18n/slugMap.ts` conceptually): `pageId → {en: path, no: path}`; consumed by the language switch, hreflang generation, sitemap, and nav. CI test asserts bijection (every EN path has exactly one NO twin and vice versa) and that every content entry maps to a route.
|
||||
|
||||
## 2. Non-page routes
|
||||
|
||||
| Route | Serves | Notes |
|
||||
|---|---|---|
|
||||
| `/cv/connor-babbington-cv-en.pdf` | English CV | Stable, shareable, versioned in repo |
|
||||
| `/cv/connor-babbington-cv-no.pdf` | Norwegian CV | Same |
|
||||
| `/api/contact` | POST → contact-relay container | Only non-static route; nginx proxies. 405 on GET |
|
||||
| `/sitemap-index.xml`, `/robots.txt` | build artifacts | SEO_SPEC |
|
||||
| `/og/*.png` | Pre-generated OG images per page per locale | Build-time generated |
|
||||
| `/Linkedin` | 301 → LinkedIn profile | Preserves the existing `cesnimda.co.uk/Linkedin` redirect used on the printed/PDF CV — must not break |
|
||||
| Legacy WP URLs (`/feed/`, `/wp-*`, old anchors) | 410 Gone (crawler cleanup) except any URL known to be shared → 301 to nearest new page | Inventory old URLs before cutover (Phase 3 task) |
|
||||
|
||||
## 3. Redirect & language behaviour
|
||||
|
||||
- **No automatic locale redirect.** `/` is always EN. Rationale (Phase 1): shared links must be stable; recruiters forward URLs across languages; auto-redirects break back-button and SEO.
|
||||
- **First-visit hint:** if `navigator.language` starts with `nb`/`nn`/`no`, and no stored preference, and current locale is EN → show the dismissible hint bar (client-side only, one-time, stores dismissal). Never shown on NO pages, never a redirect.
|
||||
- **Language switch:** header control resolves current pageId via the slug map → navigates to the twin URL. Stores preference (used only for the hint logic and CV-button default — never for redirects).
|
||||
- **Unknown `/no/...` path:** bilingual 404 (NO strings first when under `/no/`).
|
||||
|
||||
## 4. URL quality rules
|
||||
|
||||
- Lowercase, hyphenated, ASCII slugs (NO slugs transliterate: `om-meg`, not `om-mæg` — æøå avoided in URLs for shareability/encoding robustness).
|
||||
- No query parameters anywhere in canonical URLs; anchors (`#architecture` / `#arkitektur`) are localised to match section headings' explicit ids from the content model (stable across copy edits).
|
||||
- Case-study section anchors are part of the content schema (DATA_MODEL) so the mini-TOC, deep links, and language-switch section-mapping (J5) stay in sync.
|
||||
|
||||
## 5. Navigation state mapping
|
||||
|
||||
- `aria-current="page"` from exact pageId match; Projects nav item also marked current on case-study pages (section-current, underline 60% per Phase 1).
|
||||
- Homepage scroll-spy: section ids `#skills` `#projects` `#experience` `#about` `#contact` (EN) / `#kompetanse` `#prosjekter` `#erfaring` `#om-meg` `#kontakt` (NO) — defined in the slug map's anchor table, not ad hoc.
|
||||
@@ -0,0 +1,45 @@
|
||||
# SEO_SPEC.md
|
||||
|
||||
## 1. Goals & reality check
|
||||
|
||||
Primary discovery is *direct* (links from applications/LinkedIn/CV), not organic search. SEO effort therefore targets: (1) branded queries ("Connor Babbington developer") ranking the new site #1 in both languages, (2) clean link-preview cards everywhere recruiters paste URLs (Slack/Teams/LinkedIn/ATS), (3) not losing the existing domain's standing during the WordPress → new-site cutover.
|
||||
|
||||
## 2. Per-page metadata (both locales)
|
||||
|
||||
- **Titles** (patterns fixed in CONTENT_STRATEGY §7): home `Connor Babbington — Systems Developer (.NET, React) · Tønsberg, Norway` / `… Systemutvikler … · Tønsberg`; inner pages `<Page> · Connor Babbington`. ≤ 60 chars target.
|
||||
- **Descriptions:** handwritten per page per locale, ≤ 155 chars, answer who/what/where. Stored in page-meta content (DATA_MODEL §6), length CI-checked.
|
||||
- **Canonical:** self-referencing per locale page. **hreflang:** `en`, `nb`, `x-default → en` emitted from the slug map on every page — the twin-URL contract guarantees correctness; CI tests hreflang reciprocity.
|
||||
- **robots:** index,follow everywhere except 404/colophon-draft states; `robots.txt` allows all + sitemap pointer.
|
||||
|
||||
## 3. Structured data (JSON-LD, build-time)
|
||||
|
||||
| Page | Schema |
|
||||
|---|---|
|
||||
| Home (both locales) | `Person`: name, jobTitle "Systems Developer"/"Systemutvikler", address (locality Tønsberg, country NO), email, sameAs [LinkedIn, git.cesnimda.uk], knowsLanguage [en, nb], knowsAbout [C#, .NET, React, Docker, …] |
|
||||
| Case studies | `CreativeWork` (subtype `SoftwareApplication` where honest — JobTrack yes, InboxIntel as `SoftwareSourceCode`): name, description, author → Person, programmingLanguage, dateModified |
|
||||
| CV hub | `Person` + `subjectOf` links to CV files |
|
||||
| All | `WebSite` + `BreadcrumbList` on nested pages |
|
||||
|
||||
Builders are typed utilities with unit tests (TECH_SPEC §9.2) — malformed JSON-LD is worse than none.
|
||||
|
||||
## 4. OpenGraph / link previews
|
||||
|
||||
- Per-page, per-locale OG: `og:locale` (`en_GB`/`nb_NO`) + `og:locale:alternate`, title/description mirroring meta, `og:type` profile (home) / article (case studies).
|
||||
- **OG images pre-generated at build** (one template per page kind, design-system styled: dark surface, name/page title, role line, trace motif, monogram; case studies add project name + stack line). 1200×630 PNG, per locale. Twitter/X summary_large_image tags mirrored.
|
||||
- This directly fixes the current site's empty `og:title` embarrassment noted in Phase 1 research.
|
||||
|
||||
## 5. Sitemaps
|
||||
|
||||
- Build-generated sitemap index → one sitemap per locale; entries carry `xhtml:link` alternates (belt-and-braces with hreflang tags); `lastmod` from git commit dates of content files.
|
||||
- Submitted via Search Console (both properties/domains as relevant) at launch; the old WP sitemap URLs left to 404→410 after cutover inventory.
|
||||
|
||||
## 6. Migration / cutover protection
|
||||
|
||||
1. **Inventory before cutover** (Phase 3 task): crawl current WP site, list indexed URLs (`site:cesnimda.co.uk`), old anchors, and any URL known to be printed/shared (esp. `/Linkedin` redirect — preserved per ROUTING_SPEC).
|
||||
2. 301 map for anything with inbound value; 410 for WP cruft (`/feed/`, `/wp-json/`, `/comments/`…). Never soft-404.
|
||||
3. Keep domain, keep HTTPS, no `www`/apex flip during migration (site currently canonicalises to apex — retain).
|
||||
4. Verify: Search Console coverage watch for 4 weeks post-launch; link-preview spot checks in LinkedIn/Slack debuggers.
|
||||
|
||||
## 7. Performance & crawlability as SEO
|
||||
|
||||
Static HTML, content not JS-gated (crawler parity guaranteed by architecture), CWV budgets in CI (LCP < 1.8 s, CLS < 0.02) — the technical-SEO checklist is mostly satisfied *by construction*; this spec adds only the markup/meta layer above.
|
||||
@@ -0,0 +1,99 @@
|
||||
# TECH_SPEC.md
|
||||
Phase 2 — technology stack evaluation and recommendation. No implementation code; decisions + justification.
|
||||
|
||||
---
|
||||
|
||||
## 1. Constraints that drive every choice
|
||||
|
||||
1. **The site is a work sample.** Core Web Vitals, a11y, and view-source cleanliness are product requirements (Phase 1, P2 persona).
|
||||
2. **Content-heavy, interaction-light.** ~10 pages × 2 languages, one form, no user accounts, no server state. Motion is CSS-grade (Phase 1 motion system: transform/opacity only, one signature animation).
|
||||
3. **Self-hosted.** Deploys to Connor's own infrastructure (Docker, nginx reverse proxy, Gitea at git.cesnimda.uk). No vendor lock to Vercel/Netlify.
|
||||
4. **Bilingual first-class.** EN root + `/no/` localised slugs, hreflang, per-language OG (Phase 1 IA §5).
|
||||
5. **Maintainable by one person** who works daily in C#/.NET + React/TypeScript.
|
||||
|
||||
## 2. Frontend framework
|
||||
|
||||
### Recommendation: **Astro 5.x (TypeScript, strict)**
|
||||
|
||||
| Criterion | Astro | Next.js (App Router) | Plain Vite + React SPA |
|
||||
|---|---|---|---|
|
||||
| Output for this content shape | Static HTML, zero JS by default — ideal | Static export possible but carries React runtime (~85 kB+) for no interactive need | SPA: worst SEO/first-paint profile — rejected outright |
|
||||
| i18n routing (path prefix + localised slugs) | First-class built-in (`defaultLocale` root, per-locale routes), content collections pair cleanly | Requires middleware/next-intl wiring; static export + i18n routing is the App Router's weakest combination | Manual everything |
|
||||
| Performance ceiling | LCP = HTML+CSS only; 100/100 realistic | Good, but hydration cost is structural | Poor |
|
||||
| Islands for the 3 interactive spots (lightbox, form, nav overlay) | Astro islands — ship JS only where needed | Everything is React | Everything is React |
|
||||
| Docker/static hosting | Builds to `dist/` static files → nginx serves them; trivial, rock-solid | Static export OK; SSR mode would need a Node runtime container for no benefit | Static, but SPA routing needs nginx fallbacks |
|
||||
| Skill-signalling for Connor | Shows breadth; both project case studies already prove React | Matches CV React keyword | Matches React |
|
||||
| Longevity/maintenance | Content-site sweet spot; low upgrade churn | Heavier upgrade treadmill | Low-level maintenance burden |
|
||||
|
||||
**Decision: Astro.** The two case studies already prove React competence; the portfolio itself should prove *judgement* — choosing the right tool for a content site is exactly the "key decisions & trade-offs" thinking the site preaches. This decision itself becomes a line in the site's "how this site works" meta page.
|
||||
|
||||
**Interactivity model:** zero-framework islands. The three interactive components (mobile nav overlay, screenshot lightbox, contact form) are small enough for vanilla TypeScript modules/custom elements inside Astro components — no React island needed. If any grows past ~150 lines of imperative DOM code, promote it to a Preact island (3 kB) rather than React (Phase 3 escape hatch, pre-authorised here).
|
||||
|
||||
## 3. Styling system
|
||||
|
||||
### Recommendation: **Tailwind CSS 4.x + CSS custom properties for the token layer**
|
||||
|
||||
- The Phase 1 design system is token-complete (colour, spacing, type, radius, motion). Tokens live as **CSS custom properties on `:root`/`[data-theme]`** — the single source of truth; Tailwind consumes them via its `@theme` mechanism. This gives: theme switching by attribute swap (no JS re-render), tokens inspectable in dev tools (P2 will look), and Tailwind's utility ergonomics without hardcoded values.
|
||||
- Component-scoped styles (Astro's scoped `<style>`) only for the few things utilities express badly (trace motif keyframes, dot-grid pattern).
|
||||
- **Rejected:** CSS-in-JS (runtime cost, needs a framework), vanilla-extract (build complexity unjustified at this size), pure hand-rolled CSS (slower iteration; Tailwind's constraint model actually *enforces* the 10-value spacing scale).
|
||||
|
||||
## 4. Animation library
|
||||
|
||||
### Recommendation: **CSS-first; no animation library. One 2-kB hand-rolled helper.**
|
||||
|
||||
- Phase 1 motion spec animates only transform/opacity with 5 duration/easing tokens → all expressible as CSS transitions/keyframes toggled by `data-*` attributes.
|
||||
- Scroll reveals + scroll-spy: one shared `IntersectionObserver` utility (the "helper"), toggling classes. Header compression: CSS `scroll-driven animations` where supported, with the observer fallback (progressive enhancement — degrade per MOTION_GUIDELINES: content never gated).
|
||||
- Trace draw: SVG `stroke-dashoffset` keyframe. Theme/language cross-fades: CSS transitions.
|
||||
- `prefers-reduced-motion`: one global CSS layer that zeroes durations and disables translations — reduced-motion is a *stylesheet concern*, not per-component JS logic.
|
||||
- **Rejected:** GSAP (license + weight for zero need), Framer Motion (requires React), Motion One (fine, but even 5 kB is unjustified when the spec fits CSS). This restraint is itself the Phase 1 philosophy enforced at the dependency level.
|
||||
|
||||
## 5. i18n library
|
||||
|
||||
### Recommendation: **Astro built-in i18n routing + typed dictionary module (no library)**
|
||||
|
||||
- Routing: Astro's native `i18n` config (defaultLocale `en` at root, `no` prefixed). Localised slugs via the route-mapping table (ROUTING_SPEC).
|
||||
- UI strings (~60: nav, buttons, form, a11y labels): a typed TypeScript dictionary per locale with a `t()` accessor — compile-time key safety, zero runtime dependency.
|
||||
- Long-form content: **content collections with per-locale entries** (DATA_MODEL.md) — content is data, not translation strings.
|
||||
- **Rejected:** i18next (runtime library solving problems — pluralisation across 20 locales, lazy loading — this site doesn't have), astro-i18next (abandonware risk). Two locales with hand-written native copy need a mapping table, not a framework.
|
||||
|
||||
## 6. State management
|
||||
|
||||
**None.** The only client state: theme (persisted preference), language-hint dismissal, lightbox open state, form submission state. All are `localStorage` + component-local. Documented explicitly so Phase 3 doesn't import a store out of habit.
|
||||
|
||||
## 7. Contact form backend
|
||||
|
||||
The one dynamic requirement on an otherwise static site.
|
||||
|
||||
### Recommendation: **a minimal self-hosted form relay container ("contact-relay")**
|
||||
- Tiny HTTP service, one endpoint: validate → honeypot + time-trap spam checks → rate-limit by IP → forward via SMTP to connor.babbington@cesnimda.co.uk → return JSON for the A9 success state.
|
||||
- Implementation language: **ASP.NET Core minimal API (.NET 9)** — deliberately, because it puts a small piece of Connor's core stack *inside* the portfolio's own infrastructure ("even my contact form is a .NET service" — a P2 talking point), and he maintains it effortlessly. Node would also work; .NET is chosen for signalling + familiarity.
|
||||
- Runs as a second container in the same compose stack; nginx routes `POST /api/contact` to it. No database — it relays and forgets (privacy plus: state nothing, log minimal).
|
||||
- **Fallback path:** the form is progressive enhancement over a plain `mailto:` + visible e-mail address (click-to-copy). If the relay is down, recruiters still convert.
|
||||
- **Rejected:** Formspree/external SaaS (data leaves his control; undermines self-hosting story), embedding SSR into Astro just for one endpoint (forces Node runtime for the whole site).
|
||||
|
||||
## 8. Supporting tooling
|
||||
|
||||
| Concern | Choice | Note |
|
||||
|---|---|---|
|
||||
| Package manager | pnpm | Lockfile determinism, fast CI |
|
||||
| Linting/format | ESLint + Prettier (+ prettier-plugin-astro), stylelint optional | CI-enforced |
|
||||
| Type checking | `astro check` + `tsc --noEmit` strict | CI gate |
|
||||
| Testing | Vitest (unit: i18n dictionary completeness, slug map integrity, structured-data builders) + Playwright (smoke: both locales render, language switch maps correctly, form relay contract, a11y scan via axe) | Full strategy in TECH_SPEC §9 |
|
||||
| Image pipeline | Astro built-in image service at build time (sharp): AVIF/WebP + fallback, responsive srcsets, enforced dimensions (CLS = 0) | Screenshots stored as source PNGs in repo |
|
||||
| Fonts | Self-hosted variable fonts (2 families + mono per TYPOGRAPHY_GUIDE), subset incl. `æøå`, `font-display: swap`, preloaded | No Google Fonts CDN (GDPR + performance + self-host principle) |
|
||||
| Icons | Lucide (static SVG imports, tree-shaken at build — inline SVG in HTML output, no icon font, no runtime) | Matches 1.5px stroke aesthetic |
|
||||
| Analytics | None, or self-hosted Plausible/Umami later (optional container slot reserved) | No cookie banner needed either way |
|
||||
|
||||
## 9. Testing & quality gates (CI-enforced, defined here, built in Phase 3)
|
||||
|
||||
1. **Static gates:** typecheck, lint, `astro check`.
|
||||
2. **Unit (Vitest):** every dictionary key exists in both locales; every route has a locale twin in the slug map; structured-data builder outputs valid JSON-LD shape.
|
||||
3. **E2E smoke (Playwright, against the built static output + relay container):** home renders in both locales; language switch from a deep NO URL lands on the correct EN twin; CV downloads resolve (200, correct filename); form happy-path + honeypot rejection; keyboard: skip-link, nav, lightbox Esc.
|
||||
4. **Accessibility:** axe scan on all page templates, both themes — zero critical violations is a merge gate.
|
||||
5. **Performance budget (Lighthouse CI):** LCP < 1.8 s (simulated 4G), CLS < 0.02, TBT < 100 ms, total JS < 40 kB gzip, per-page HTML+CSS < 100 kB. Budgets fail the pipeline, not just warn.
|
||||
|
||||
## 10. Summary of the stack
|
||||
|
||||
**Astro 5 + TypeScript · Tailwind 4 over CSS custom-property tokens · CSS-first motion (no animation dependency) · built-in i18n + typed dictionaries + content collections · zero client state libraries · .NET 9 minimal-API contact relay · pnpm/Vitest/Playwright/Lighthouse CI · static output behind nginx, all self-hosted in Docker.**
|
||||
|
||||
Every "no" above (no React runtime, no animation library, no i18n framework, no CMS, no SaaS form) is a deliberate scope-fit decision and should be written up on the site's meta page as evidence of production judgement.
|
||||
Reference in New Issue
Block a user