# Dark & Technical Redesign Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Rebuild the dzanan.net UI in the approved "Dark & technical" direction, drop the CV page in favour of a PDF link, and keep SSR, SEO and tests green. **Architecture:** Angular 22 standalone components with inline templates; all visual styling lives in one global `src/styles.scss` driven by CSS custom properties (the codebase's existing pattern). Pages compose the shared `PageShellComponent`. Content data stays in `portfolio-projects.data.ts`. Express (`src/server.ts`) serves static assets and adds one redirect. **Tech Stack:** Angular 22.1, @angular/ssr + Express 5, SCSS, Tailwind v4 (installed, unused by handwritten styles), Karma/Jasmine, self-hosted fonts via `@fontsource-variable/jetbrains-mono` and `@fontsource/ibm-plex-sans`. **Spec:** `docs/superpowers/specs/2026-09-06-dark-technical-redesign-design.md` ## Global Constraints - Angular 22.1.x, Node ≥ 22.22.3 (24.15 in the cloud workspace); use `ng` via `npx`. - No light theme; no `.dark` selectors; `color-scheme: dark`. - Tokens exactly as the spec table: `--bg #0D1015`, `--panel #12161D`, `--panel-2 #0F1318`, `--line #232A33`, `--line-strong #2A323D`, `--text #E6EAF0`, `--muted #8B95A5`, `--dim #5C6672`, `--accent #8BE04A`, `--accent-glow rgba(139,224,74,.22)`, `--code-key #7AA2F7`, `--code-str #C3E88D`, `--code-num #F78C6C`. - Fonts self-hosted (no Google Fonts requests). Mono = JetBrains Mono; sans = IBM Plex Sans. - Labels are prefixed `// ` in markup and use `.label`. - Case-study text content unchanged; `PORTFOLIO_PROJECTS` must not contain "Unija" (existing test). - Every task ends with `npx ng build` still succeeding or tests passing as stated. - Commit after each task with the trailer: `Co-Authored-By: Claude Fable 5.1 ` and `Claude-Session: https://claude.ai/code/session_01LoRUjpFxfDpRFGEUrtEzP4`. - Unit tests run headless: `CHROME_BIN= npx ng test --watch=false --browsers=ChromeHeadlessNoSandbox` after Task 1 adds the launcher. --- ### Task 1: Fonts, tokens and base stylesheet **Files:** - Modify: `package.json` (devDependencies unaffected; dependencies gain two font packages) - Modify: `angular.json` (`styles` arrays in `build.options`, and `test.options`) - Modify: `src/index.html` - Rewrite: `src/styles.scss` - Modify: `karma.conf` — none exists; add `ChromeHeadlessNoSandbox` via `angular.json` test `browsers`? Karma builder accepts `--browsers` and a custom launcher needs a config file. Create `karma.conf.cjs` and point `test.options.karmaConfig` at it. **Interfaces:** - Produces: global classes used by every later task — `.shell`, `.mono`, `.label`, `.panel`, `.tag`, `.tag-row`, `.btn`, `.btn-accent`, `.btn-line`, `.section`, `.section-head`, `.eyebrow-row`, `.status-dot`, `.icon`, plus component-specific classes named in Tasks 3–7 (they are all defined here so the stylesheet is written once). - [x] **Step 1: Install fonts** ```bash npm install @fontsource-variable/jetbrains-mono@5.3.0 @fontsource/ibm-plex-sans@5.3.0 --no-audit --no-fund ``` - [x] **Step 2: Register font CSS and karma config in `angular.json`** In `projects.dzanan.net.architect.build.options.styles` replace the array with: ```json "styles": [ "node_modules/@fontsource-variable/jetbrains-mono/index.css", "node_modules/@fontsource/ibm-plex-sans/400.css", "node_modules/@fontsource/ibm-plex-sans/500.css", "node_modules/@fontsource/ibm-plex-sans/600.css", "src/styles.scss" ] ``` Do the same in `architect.test.options.styles`, and add `"karmaConfig": "karma.conf.cjs"` to `architect.test.options`. Create `karma.conf.cjs`: ```js module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular/build:unit-test'.length ? 'jasmine' : 'jasmine'], plugins: [require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage')], client: { jasmine: {}, clearContext: false }, reporters: ['progress'], browsers: ['ChromeHeadlessNoSandbox'], customLaunchers: { ChromeHeadlessNoSandbox: { base: 'ChromeHeadless', flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'] } }, restartOnFileChange: false }); }; ``` (The `frameworks` line is just `['jasmine']`; keep it simple: `frameworks: ['jasmine']`.) - [x] **Step 3: Update `src/index.html`** ```html Amar Džanan — Senior Software Engineer & AI Integration ``` - [x] **Step 4: Rewrite `src/styles.scss`** ```scss @use 'tailwindcss'; :root { color-scheme: dark; --bg: #0D1015; --panel: #12161D; --panel-2: #0F1318; --line: #232A33; --line-strong: #2A323D; --text: #E6EAF0; --muted: #8B95A5; --dim: #5C6672; --accent: #8BE04A; --accent-glow: rgba(139, 224, 74, .22); --code-key: #7AA2F7; --code-str: #C3E88D; --code-num: #F78C6C; --mono: "JetBrains Mono Variable", "JetBrains Mono", Consolas, Menlo, monospace; --sans: "IBM Plex Sans", "Segoe UI", system-ui, -apple-system, sans-serif; --radius: 10px; } * { box-sizing: border-box; } html { scroll-behavior: smooth; scroll-padding-top: 6rem; } body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--sans); font-size: 16px; line-height: 1.5; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } a { color: inherit; text-decoration: none; } a:hover { color: var(--accent); } button, a { -webkit-tap-highlight-color: transparent; } button:focus-visible, a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; } ::selection { background: var(--accent); color: var(--bg); } h1, h2, h3, h4, p { margin: 0; } h1, h2 { font-weight: 600; letter-spacing: -.03em; text-wrap: balance; } h1 { font-size: clamp(2.4rem, 5vw, 3.9rem); line-height: 1.04; } h2 { font-size: clamp(1.75rem, 3vw, 2.1rem); line-height: 1.1; letter-spacing: -.025em; } h3 { font-size: 1.25rem; font-weight: 500; letter-spacing: -.01em; line-height: 1.25; } h4 { font-size: 1rem; font-weight: 500; } /* ---- primitives ---- */ .site-frame { min-height: 100vh; background-color: var(--bg); background-image: linear-gradient(rgba(255, 255, 255, .028) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, .028) 1px, transparent 1px); background-size: 64px 64px; } .shell { width: min(1160px, calc(100% - 40px)); margin-inline: auto; } .mono { font-family: var(--mono); } .label { font-family: var(--mono); font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); } .dim { color: var(--dim); } .muted { color: var(--muted); } .panel { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); } .tag-row { display: flex; flex-wrap: wrap; gap: 6px; } .tag { display: inline-flex; align-items: center; height: 24px; padding: 0 8px; border-radius: 4px; border: 1px solid var(--line-strong); color: var(--muted); font: 400 12px/1 var(--mono); white-space: nowrap; } .btn { display: inline-flex; align-items: center; gap: 10px; height: 46px; padding: 0 20px; border-radius: 6px; font: 500 14px/1 var(--mono); white-space: nowrap; transition: background-color .15s ease, color .15s ease, box-shadow .15s ease, border-color .15s ease; } .btn-accent { background: var(--accent); color: var(--bg); box-shadow: 0 0 0 1px rgba(139, 224, 74, .4), 0 0 32px var(--accent-glow); } .btn-accent:hover { color: var(--bg); background: #A9F26F; box-shadow: 0 0 0 1px rgba(139, 224, 74, .6), 0 0 40px var(--accent-glow); } .btn-line { border: 1px solid var(--line-strong); color: var(--text); background: rgba(255, 255, 255, .02); } .btn-line:hover { color: var(--text); border-color: var(--accent); background: rgba(139, 224, 74, .06); } .icon { width: 14px; height: 14px; flex: none; } .status-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px var(--accent); } .skip-link { position: fixed; left: 1rem; top: -5rem; z-index: 100; background: var(--accent); color: var(--bg); padding: .7rem 1rem; font-family: var(--mono); font-size: 13px; border-radius: 6px; } .skip-link:focus { top: 1rem; } .section { padding-block: 96px; } .section-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; margin-bottom: 24px; } .section-head .label { display: block; margin-bottom: 10px; } .section-head .count { font: 400 12px/1 var(--mono); color: var(--dim); } /* ---- header / footer ---- */ .site-header { position: sticky; top: 0; z-index: 50; border-bottom: 1px solid var(--line); background: rgba(13, 16, 21, .92); backdrop-filter: blur(12px); } .header-inner { min-height: 72px; display: flex; align-items: center; justify-content: space-between; gap: 24px; } .wordmark { display: inline-flex; align-items: center; gap: 10px; font: 600 14px/1 var(--mono); } .wordmark:hover { color: var(--text); } .wordmark-tilde { color: var(--accent); font-weight: 400; } .desktop-nav { display: flex; align-items: center; gap: 28px; } .desktop-nav nav { display: flex; gap: 28px; } .desktop-nav nav a, .mobile-nav nav a, .nav-cv { color: var(--muted); font: 500 13px/1 var(--mono); letter-spacing: .02em; } .desktop-nav nav a:hover, .mobile-nav nav a:hover, .nav-cv:hover { color: var(--accent); } .nav-cv { display: inline-flex; align-items: center; gap: 6px; } .status { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font: 400 12px/1 var(--mono); padding-left: 20px; border-left: 1px solid var(--line); } .menu-button { display: none; align-items: center; gap: 12px; border: 0; background: transparent; color: var(--text); font: 500 13px/1 var(--mono); cursor: pointer; } .mobile-nav { display: none; border-top: 1px solid var(--line); padding-block: 16px 20px; } .mobile-nav nav { display: grid; gap: 14px; } .site-footer { border-top: 1px solid var(--line); padding-block: 28px; margin-top: 40px; } .footer-inner { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; color: var(--dim); font: 400 12px/1.6 var(--mono); } .footer-inner a { color: var(--muted); } /* ---- hero ---- */ .hero { display: grid; grid-template-columns: 1.05fr .95fr; gap: 64px; align-items: center; padding-block: 88px 96px; } .hero-copy { display: flex; flex-direction: column; gap: 26px; } .hero-copy .label { letter-spacing: .06em; } .hero-copy h1 span { color: var(--accent); } .hero-lead { max-width: 560px; color: var(--muted); font-size: 18px; line-height: 1.6; } .hero-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 4px; } .terminal { padding: 0; overflow: hidden; box-shadow: 0 30px 80px rgba(0, 0, 0, .5); font-family: var(--mono); } .terminal-bar { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--panel-2); } .terminal-dots { display: flex; gap: 6px; } .terminal-dots span { width: 10px; height: 10px; border-radius: 50%; background: var(--line-strong); } .terminal-title { color: var(--dim); font-size: 12px; } .terminal-body { padding: 22px 24px; font-size: 14px; line-height: 1.75; display: flex; flex-direction: column; overflow-x: auto; } .terminal-body .k { color: var(--code-key); } .terminal-body .s { color: var(--code-str); } .terminal-body .n { color: var(--code-num); } .terminal-body .c { color: var(--dim); } .terminal-line { white-space: pre; } .terminal-line.indent { padding-left: 2ch; } .cursor { display: inline-block; width: 9px; height: 16px; background: var(--accent); vertical-align: -3px; margin-left: 2px; } /* ---- work list ---- */ .work-list { display: flex; flex-direction: column; overflow: hidden; } .project-row { display: grid; grid-template-columns: 56px minmax(0, 1fr) 300px 40px; gap: 24px; padding: 26px 24px; border-top: 1px solid var(--line); align-items: center; color: var(--text); transition: background-color .15s ease; } .project-row:first-child { border-top: 0; } .project-row:hover { background: var(--panel-2); color: var(--text); } .project-index { font: 400 13px/1 var(--mono); color: var(--dim); } .project-row.featured .project-index, .project-row.featured .project-arrow { color: var(--accent); } .project-title { display: flex; flex-direction: column; gap: 6px; } .project-title p { color: var(--muted); font-size: 14.5px; } .project-arrow { color: var(--dim); text-align: right; font-size: 18px; transition: color .15s ease, transform .15s ease; } .project-row:hover .project-arrow { color: var(--accent); transform: translateX(4px); } .section-head.padded { padding-inline: 24px; } /* ---- experience ---- */ .experience { display: grid; grid-template-columns: 340px 1fr; gap: 64px; align-items: start; } .experience-intro { display: flex; flex-direction: column; gap: 18px; } .experience-intro p { color: var(--muted); font-size: 15px; line-height: 1.6; } .experience-intro .text-link { align-self: flex-start; } .portrait { width: 96px; height: 96px; border-radius: 8px; object-fit: cover; display: block; filter: grayscale(1) contrast(1.05); border: 1px solid var(--line); } .text-link { display: inline-flex; align-items: center; gap: 6px; color: var(--accent); font: 500 13px/1 var(--mono); } .text-link:hover { color: #A9F26F; } .roles { display: flex; flex-direction: column; } .role { display: grid; grid-template-columns: 170px 1fr 1.2fr; gap: 28px; padding: 20px 0; border-top: 1px solid var(--line); align-items: baseline; } .role:last-child { border-bottom: 1px solid var(--line); } .role time { color: var(--dim); font: 400 13px/1.4 var(--mono); } .role h4 { font-size: 16px; } .role .company { display: block; color: var(--muted); font-size: 13.5px; margin-top: 2px; } .role p { color: var(--muted); font-size: 14.5px; line-height: 1.5; } /* ---- capabilities ---- */ .capabilities { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } .capability { padding: 28px; display: flex; flex-direction: column; gap: 14px; } .capability .index { font: 400 12px/1 var(--mono); color: var(--accent); } .capability p { color: var(--muted); font-size: 14.5px; line-height: 1.55; } /* ---- contact ---- */ .contact { padding: clamp(32px, 5vw, 56px); border-color: rgba(139, 224, 74, .35); box-shadow: inset 0 0 80px rgba(139, 224, 74, .05); display: flex; flex-direction: column; gap: 22px; } .contact h2 { max-width: 760px; } .contact-actions { display: flex; justify-content: space-between; align-items: center; gap: 24px; flex-wrap: wrap; margin-top: 8px; } .contact-actions .meta { color: var(--dim); font: 400 12px/1 var(--mono); } /* ---- case study ---- */ .breadcrumb { display: flex; gap: 0; font: 400 13px/1 var(--mono); color: var(--text); margin-bottom: 28px; } .breadcrumb a { color: var(--dim); } .breadcrumb a:hover { color: var(--accent); } .case-hero { padding-block: 64px 48px; display: flex; flex-direction: column; gap: 22px; } .case-hero h1 { max-width: 900px; } .case-hero .lead { max-width: 760px; color: var(--muted); font-size: 18px; line-height: 1.6; } .case-meta { display: grid; grid-template-columns: auto auto 1fr; gap: 32px; padding: 18px 24px; align-items: center; } .case-meta dt { font: 400 11px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--dim); margin-bottom: 8px; } .case-meta dd { margin: 0; font-size: 14.5px; } .case-section { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 40px; padding-block: 56px; border-top: 1px solid var(--line); } .case-section .label { padding-top: 6px; } .case-section h2 { margin-bottom: 20px; } .large-copy { max-width: 720px; color: var(--muted); font-size: 17px; line-height: 1.65; } .contribution-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; } .contribution-list li { display: grid; grid-template-columns: 40px 1fr; gap: 16px; padding: 16px 0; border-top: 1px solid var(--line); } .contribution-list li:last-child { border-bottom: 1px solid var(--line); } .contribution-list li span { font: 400 13px/1.6 var(--mono); color: var(--accent); } .contribution-list li p { color: var(--muted); font-size: 15px; line-height: 1.6; } .decision-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } .decision { padding: 24px; display: flex; flex-direction: column; gap: 12px; } .decision span { font: 400 12px/1 var(--mono); color: var(--accent); } .decision p { color: var(--muted); font-size: 14.5px; line-height: 1.55; } .diagram-wrap { padding-block: 8px 56px; } .system-diagram { margin: 0; padding: 24px; display: flex; flex-direction: column; gap: 18px; } .system-diagram figcaption { display: flex; align-items: baseline; gap: 16px; } .system-diagram figcaption .mono { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); } .system-diagram figcaption strong { font-size: 15px; font-weight: 500; } .system-diagram svg { width: 100%; height: auto; display: block; font-family: var(--mono); } .diagram-lines path { fill: none; stroke: var(--line-strong); stroke-width: 1.5; } .diagram-node rect { fill: var(--panel-2); stroke: var(--line-strong); stroke-width: 1.5; rx: 6; } .diagram-node text { fill: var(--text); font-size: 14px; font-weight: 500; text-anchor: middle; letter-spacing: .04em; } .diagram-node text.small { fill: var(--muted); font-size: 11px; font-weight: 400; } .diagram-node.accent rect { stroke: var(--accent); fill: rgba(139, 224, 74, .06); } .diagram-node.accent text { fill: var(--accent); } .diagram-node.accent text.small { fill: var(--code-str); } .diagram-note text { fill: var(--dim); font-size: 11px; letter-spacing: .1em; text-anchor: middle; } .case-pagination { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; padding-block: 16px 64px; } .case-pagination a { padding: 22px 24px; display: flex; flex-direction: column; gap: 10px; color: var(--text); transition: border-color .15s ease; } .case-pagination a:hover { border-color: var(--accent); color: var(--text); } .case-pagination a .mono { font-size: 12px; color: var(--dim); } .case-pagination a:last-child { text-align: right; } .case-pagination strong { font-weight: 500; font-size: 15px; } .case-contact { margin-bottom: 64px; } /* ---- not found ---- */ .not-found { padding-block: 96px; display: flex; flex-direction: column; gap: 24px; max-width: 720px; } .not-found .terminal-body { gap: 4px; } .not-found p.muted { font-size: 16px; line-height: 1.6; } /* ---- motion ---- */ @media (prefers-reduced-motion: no-preference) { .hero-copy > * { animation: rise .48s ease both; } .hero-copy > :nth-child(2) { animation-delay: .08s; } .hero-copy > :nth-child(3) { animation-delay: .16s; } .hero-copy > :nth-child(4) { animation-delay: .24s; } .terminal { animation: rise .6s ease .2s both; } .cursor { animation: blink 1s steps(1) infinite; } } @keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } } @keyframes blink { 50% { opacity: 0; } } /* ---- responsive ---- */ @media (max-width: 1024px) { .hero { grid-template-columns: 1fr; gap: 40px; padding-block: 56px 64px; } .experience { grid-template-columns: 1fr; gap: 32px; } .project-row { grid-template-columns: 48px minmax(0, 1fr) 40px; } .project-row .tag-row { grid-column: 2; } .case-section { grid-template-columns: 1fr; gap: 16px; } } @media (max-width: 720px) { .section { padding-block: 64px; } .desktop-nav { display: none; } .menu-button { display: inline-flex; } .mobile-nav { display: block; } .hero-copy h1 { font-size: clamp(2rem, 9vw, 2.6rem); } .project-row { grid-template-columns: minmax(0, 1fr) 24px; padding: 20px 18px; gap: 10px 12px; } .project-index { display: none; } .project-row .tag-row { grid-column: 1 / -1; } .role { grid-template-columns: 1fr; gap: 6px; } .capabilities, .decision-grid, .case-pagination { grid-template-columns: 1fr; } .case-pagination a:last-child { text-align: left; } .case-meta { grid-template-columns: 1fr; gap: 16px; } .section-head { flex-direction: column; align-items: flex-start; } .terminal-body { font-size: 12.5px; padding: 16px; } } ``` - [x] **Step 5: Build to verify the stylesheet compiles** Run: `npx ng build 2>&1 | tail -5` Expected: "Application bundle generation complete" (templates still reference old classes — that's fine, they'll be rewritten). - [x] **Step 6: Commit** ```bash git add package.json package-lock.json angular.json karma.conf.cjs src/index.html src/styles.scss git commit -m "feat(ui): dark-technical tokens, self-hosted fonts, base stylesheet" ``` --- ### Task 2: Remove theme + CV page; `/cv` → `/cv.pdf` **Files:** - Modify: `src/app/app.routes.spec.ts`, `src/app/app.routes.ts`, `src/app/app.routes.server.ts`, `src/app/app.ts`, `src/server.ts`, `public/sitemap.xml` - Delete: `src/app/features/cv/cv-page.component.ts`, `src/app/features/cv/cv-page.component.spec.ts`, `src/app/core/services/theme.service.ts`, `src/app/shared/ui/theme-toggle/theme-toggle.component.ts` - Create: `public/cv.pdf` (placeholder) **Interfaces:** - Produces: route list without `cv`; `/cv.pdf` static file; nothing else depends on it. - [x] **Step 1: Update the route spec (failing)** In `src/app/app.routes.spec.ts` change the expected path list to: ```ts expect(routes.map(route => route.path)).toEqual([ '', 'work/secure-ai-integration-gateway', 'work/travel-operations-platform', 'work/service-operations-platform', 'work/biodiversity-data-platform', 'portfolio', '**' ]); ``` - [x] **Step 2: Run tests to see it fail** Run: `CHROME_BIN=$CHROME npx ng test --watch=false 2>&1 | tail -20` — expected: 1 FAILED (routes contain `cv`). (`$CHROME` = Playwright chromium binary; see Global Constraints.) - [x] **Step 3: Remove the `cv` route and theme wiring** `src/app/app.routes.ts`: delete the `{ path: 'cv', ... }` object. `src/app/app.routes.server.ts`: delete the `{ path: 'cv', renderMode: RenderMode.Prerender }` line. `src/app/app.ts`: ```ts import { ChangeDetectionStrategy, Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet], changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './app.html' }) export class App {} ``` Delete the four files listed above (`git rm`). `PageShellComponent` still imports the toggle — Task 3 fixes it; for this task's test run, remove `ThemeToggleComponent` from its `imports` array and the two `` tags now. - [x] **Step 4: Server redirect + sitemap + placeholder PDF** `src/server.ts`, insert before the `express.static` block: ```ts /** The CV is a static PDF; keep the old /cv URL working. */ app.get('/cv', (_req, res) => { res.redirect(301, '/cv.pdf'); }); ``` `public/sitemap.xml`: remove the `https://dzanan.net/cv` line. Placeholder PDF (until Amar supplies the real one), generated with Python: ```bash python3 - <<'EOF' from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas c = canvas.Canvas('public/cv.pdf', pagesize=A4) c.setFont('Helvetica-Bold', 22); c.drawString(72, 760, 'Amar Dzanan') c.setFont('Helvetica', 12); c.drawString(72, 736, 'CV coming soon - amar@dzanan.net') c.save() EOF ``` (If reportlab is missing: `pip install reportlab --break-system-packages`.) - [x] **Step 5: Run tests — pass** Run: `CHROME_BIN=$CHROME npx ng test --watch=false 2>&1 | tail -5` — expected: all SUCCESS (the landing spec still expects `a[href="/cv"]`, which still exists until Task 5 — it passes for now). - [x] **Step 6: Commit** ```bash git add -A git commit -m "feat: drop CV page and theme toggle; redirect /cv to /cv.pdf" ``` --- ### Task 3: Navigation config and page shell **Files:** - Modify: `src/app/shared/config/navigation.config.ts`, `src/app/core/layout/page-shell.component.ts` **Interfaces:** - Produces: `MAIN_NAVIGATION: SectionNavItem[]` = work / experience / contact; `PageShellComponent` with `@Input() sections`, unchanged selector `app-page-shell`. - [x] **Step 1: Navigation config** ```ts import { SectionNavItem } from '../models/section-nav.model'; export const MAIN_NAVIGATION: SectionNavItem[] = [ { label: 'work', path: '/', fragment: 'work' }, { label: 'experience', path: '/', fragment: 'experience' }, { label: 'contact', path: '/', fragment: 'contact' } ]; export const CV_URL = '/cv.pdf'; ``` - [x] **Step 2: Page shell** ```ts import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'; import { RouterLink } from '@angular/router'; import { SectionNavItem } from '../../shared/models/section-nav.model'; import { CV_URL } from '../../shared/config/navigation.config'; @Component({ selector: 'app-page-shell', standalone: true, imports: [RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: `
` }) export class PageShellComponent { @Input({ required: true }) sections: SectionNavItem[] = []; readonly cvUrl = CV_URL; readonly menuOpen = signal(false); toggleMenu(): void { this.menuOpen.update(open => !open); } closeMenu(): void { this.menuOpen.set(false); } } ``` - [x] **Step 3: Build** Run: `npx ng build 2>&1 | grep -E "error|complete"` — expected: "complete", no errors. - [x] **Step 4: Commit** ```bash git add src/app/shared/config/navigation.config.ts src/app/core/layout/page-shell.component.ts git commit -m "feat(shell): mono header with cv.pdf link and availability status" ``` --- ### Task 4: `tagline` on portfolio projects **Files:** - Modify: `src/app/features/work/portfolio-project.model.ts`, `src/app/features/work/portfolio-projects.data.ts`, `src/app/features/work/portfolio-projects.data.spec.ts` **Interfaces:** - Produces: `PortfolioProject.tagline: string` (≤ 70 chars, no trailing period). - [x] **Step 1: Failing test** — add inside the `for (const project of PORTFOLIO_PROJECTS)` loop in the data spec: ```ts expect(project.tagline.length).toBeGreaterThan(10); expect(project.tagline.length).toBeLessThanOrEqual(70); ``` - [x] **Step 2: Run** — expected: TypeScript error `tagline does not exist` (compile failure counts as red). - [x] **Step 3: Implement** — model: add `readonly tagline: string;` after `summary`. Data, one per project (insert after each `summary:` value): | id | tagline | |---|---| | ai-integration | `Production MCP service · validated boundaries · verified writes` | | travel-operations | `Products, departures, reservations, finance, partner systems` | | service-operations | `Tickets, worklogs, contracts, accounting · CQRS · SignalR` | | biodiversity-data | `Taxonomy, sampling, specimens, habitats, map-based GIS` | - [x] **Step 4: Run tests** — expected: SUCCESS. - [x] **Step 5: Commit** — `git commit -am "feat(data): short taglines for the work list"` --- ### Task 5: Landing page **Files:** - Modify: `src/app/features/landing/landing-page.component.spec.ts`, `src/app/features/landing/landing-page.component.ts` - [x] **Step 1: Update spec (failing)** — replace the two `expect`s that change: ```ts expect(page.querySelector('h1')?.textContent).toContain('Complex systems, understood before changed.'); // ... expect(page.querySelector('a[href="/cv.pdf"]')).toBeTruthy(); ``` and add to the first test: `expect(page.querySelector('[data-testid="profile-terminal"]')?.textContent).toContain('"open_to"');` - [x] **Step 2: Run** — expected: FAIL on h1 text. - [x] **Step 3: Implement `landing-page.component.ts`** ```ts import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { NgOptimizedImage } from '@angular/common'; import { RouterLink } from '@angular/router'; import { PageShellComponent } from '../../core/layout/page-shell.component'; import { SeoService } from '../../core/services/seo.service'; import { CV_URL, MAIN_NAVIGATION } from '../../shared/config/navigation.config'; import { PORTFOLIO_PROJECTS } from '../work/portfolio-projects.data'; type TerminalValue = { kind: 'str'; value: string } | { kind: 'num'; value: number } | { kind: 'list'; value: string[] }; @Component({ selector: 'app-landing-page', standalone: true, imports: [PageShellComponent, NgOptimizedImage, RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: `

Senior full-stack engineer — .NET · Angular · MCP

Complex systems, understood before changed.

I modernize enterprise software and connect it to AI through narrow, validated, verified interfaces — the kind you can run in production and sleep at night.

profile.json
$ whoami --json
{{ '{' }}
@for (entry of profile; track entry.key; let last = $last) {
"{{ entry.key }}": @switch (entry.value.kind) { @case ('str') { "{{ entry.value.value }}" } @case ('num') { {{ entry.value.value }} } @case ('list') { [@for (item of entry.value.value; track item; let lastItem = $last) {"{{ item }}"@if (!lastItem) {, }}] } }@if (!last) {,}
}
{{ '}' }}
$
// selected work

Production systems, anonymized.

{{ projects.length }} case studies
Portrait of Amar Džanan // experience

Eight years, one product at a time.

Senior software engineer and product owner — most useful where business rules, legacy systems and new capabilities meet. I turn incomplete requirements into maintainable paths through the UI, domain model, integrations, deployment and operations.

LinkedIn ↗
@for (role of experience; track role.period + role.title) {

{{ role.title }}

{{ role.company }}

{{ role.focus }}

}
// capabilities

From uncertain problem to running system.

@for (capability of capabilities; track capability.number) {
{{ capability.number }}

{{ capability.title }}

{{ capability.detail }}

}
// contact

Need a senior engineer who can understand the system before changing it?

amar@dzanan.netSarajevo · CET / CEST
` }) export class LandingPageComponent { readonly sections = MAIN_NAVIGATION; readonly cvUrl = CV_URL; readonly projects = PORTFOLIO_PROJECTS; readonly profile: { key: string; value: TerminalValue }[] = [ { key: 'name', value: { kind: 'str', value: 'Amar Džanan' } }, { key: 'role', value: { kind: 'str', value: 'Senior Full-Stack Engineer · Product Owner' } }, { key: 'stack', value: { kind: 'list', value: ['.NET', 'Angular', 'TypeScript'] } }, { key: 'focus', value: { kind: 'list', value: ['modernization', 'safe AI integration'] } }, { key: 'since', value: { kind: 'num', value: 2018 } }, { key: 'location', value: { kind: 'str', value: 'Sarajevo · CET' } }, { key: 'open_to', value: { kind: 'list', value: ['senior roles', 'consulting'] } } ]; readonly experience = [ { period: '2025 — now', company: 'Unija Smart Accounting BiH', title: 'Senior Software Engineer', focus: 'Enterprise platforms, modernization, automation and AI integration.' }, { period: '2023 — now', company: 'COMP-2000', title: 'Product Owner', focus: 'Product direction, backlog ownership, cross-functional delivery and user feedback.' }, { period: '2020 — now', company: 'COMP-2000', title: 'Software Developer', focus: 'Full-stack .NET, Angular, mobile, APIs, data and production support.' }, { period: '2018 — 2020', company: 'COMP-2000', title: 'Junior Software Developer', focus: 'C#, SQL, desktop, web, mobile and application lifecycle foundations.' } ]; readonly capabilities = [ { number: '01', title: 'System modernization', detail: 'Upgrade frameworks, identity, routing, delivery and diagnostics without losing working domain knowledge.' }, { number: '02', title: 'Full-stack product delivery', detail: 'Carry business workflows through interface design, APIs, domain rules, persistence, integrations and tests.' }, { number: '03', title: 'Safe AI integration', detail: 'Expose useful capabilities through narrow tools, validated boundaries, verified writes and observable operations.' } ]; constructor() { inject(SeoService).update({ title: 'Amar Džanan — Senior Software Engineer & AI Integration', description: 'Senior full-stack engineer modernizing complex enterprise software and connecting it safely to AI.', path: '/', structuredData: { '@context': 'https://schema.org', '@type': 'Person', name: 'Amar Džanan', url: 'https://dzanan.net/', email: 'mailto:amar@dzanan.net', jobTitle: 'Senior Software Engineer', sameAs: ['https://www.linkedin.com/in/amardzanan/'], knowsAbout: ['.NET', 'Angular', 'TypeScript', 'Software architecture', 'Model Context Protocol', 'AI integration'] } }); } } ``` - [x] **Step 4: Run tests** — expected: SUCCESS. - [x] **Step 5: Commit** — `git commit -am "feat(landing): dark-technical hero, work list, experience, capabilities, contact"` --- ### Task 6: Case study page + diagram recolour **Files:** - Modify: `src/app/features/portfolio/portfolio-page.component.spec.ts`, `src/app/features/portfolio/portfolio-page.component.ts` - `system-diagram.component.ts`: no change needed — its classes (`diagram-lines`, `diagram-node`, `accent`, `diagram-note`, `small`) are restyled in Task 1. Only wrap `
` with class `panel`: change `class="system-diagram"` to `class="system-diagram panel"`. - [x] **Step 1: Spec (failing)** — add `expect(page.querySelector('a[href="/cv.pdf"]')).toBeTruthy();` and `expect(page.textContent).toContain('// 01 context');`. - [x] **Step 2: Run** — expected: FAIL. - [x] **Step 3: Implement template** (class and constructor unchanged; add `readonly cvUrl = CV_URL;` and import `CV_URL`): ```html

{{ project.category }} · {{ project.period }}

{{ project.title }}

{{ project.summary }}

role
{{ project.role }}
period
{{ project.period }}
stack
@for (technology of project.stack; track technology) {{{ technology }}}
// 01 context

The challenge

{{ project.challenge }}

// 02 delivery

What I contributed

    @for (contribution of project.contributions; track contribution; let index = $index) {
  1. {{ contributionNumber(index) }}

    {{ contribution }}

  2. }
// 03 architecture

Key decisions

@for (decision of project.decisions; track decision.title; let index = $index) {
{{ contributionNumber(index) }}

{{ decision.title }}

{{ decision.detail }}

}
// 04 result

The outcome

{{ project.outcome }}

// contact

Have a complex system? Let's make the next change safer and more useful.

``` - [x] **Step 4: Run tests** — expected: SUCCESS. - [x] **Step 5: Commit** — `git commit -am "feat(work): dark-technical case study layout and diagram recolour"` --- ### Task 7: Not-found page **Files:** `src/app/features/not-found/not-found-page.component.ts` - [x] **Step 1: Implement** ```html
404
$ open {{ path }}
command not found: {{ path }}
$

This path ends here.

The page may have moved. The selected systems and the CV are one click away.

cd ~/
``` Component gets `readonly path = inject(DOCUMENT).location?.pathname ?? '/unknown';` — use `inject(DOCUMENT)` from `@angular/common` (SSR-safe: on the server `document.location` exists in Domino as the request URL). - [x] **Step 2: Build + commit** — `npx ng build`; `git commit -am "feat(404): terminal-style not-found page"` --- ### Task 8: Verification - [x] Unit tests: `CHROME_BIN=$CHROME npx ng test --watch=false` → all SUCCESS. - [x] Production build: `npx ng build` → zero errors, prerendered 5 routes, budgets not exceeded. - [x] Serve: `PORT=4100 node dist/dzanan.net/server/server.mjs &`; `curl -sI localhost:4100/cv | head -3` → `301` + `Location: /cv.pdf`; `curl -sI localhost:4100/cv.pdf | head -1` → 200; `curl -s localhost:4100/nope -o /dev/null -w "%{http_code}"` → 404. - [x] Screenshots with Playwright at 1440×900 and 390×844 of `/`, `/work/secure-ai-integration-gateway`, `/nope`; view them; fix wrapping/overflow issues and re-run. - [x] Commit fixes; update the plan checkboxes.