Ralph iteration 1: work in progress

This commit is contained in:
2026-02-10 15:15:41 +00:00
parent d788d2e4cf
commit 4c1af9ecaf
17 changed files with 800 additions and 925 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(powershell -Command \"$lines = Get-Content ''4-vitals-monitor.html''; $before = $lines[0..1757]; $after = $lines[2028..\\($lines.Length-1\\)]; \\($before + $after\\) | Set-Content ''4-vitals-monitor.html'' -Encoding UTF8\")",
"Bash(powershell -Command \"$lines = Get-Content ''4-vitals-monitor.html''; $before = $lines[0..1757]; $after = $lines[2028..\\($lines.Length-1\\)]; $result = $before + $after; $result | Set-Content ''4-vitals-monitor.html'' -Encoding UTF8\")",
"Bash(powershell -ExecutionPolicy Bypass -File:*)",
"Bash(del \"C:\\\\Users\\\\Andy\\\\Ralph Local\\\\Tasks\\\\cv-4-vitals-monitor\\\\remove-lines.ps1\")",
"Bash(start \"\" \"C:\\\\Users\\\\Andy\\\\Ralph Local\\\\Tasks\\\\cv-4-vitals-monitor\\\\4-vitals-monitor.html\")"
]
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"iterations": [
{
"iteration": 1,
"startedAt": "2026-02-10T15:15:40.340Z",
"endedAt": "2026-02-10T15:15:41.803Z",
"durationMs": 1054,
"toolsUsed": {},
"filesModified": [],
"exitCode": 0,
"completionDetected": false,
"errors": [
"ProviderModelNotFoundError: ProviderModelNotFoundError"
]
}
],
"totalDurationMs": 1054,
"struggleIndicators": {
"repeatedErrors": {
"ProviderModelNotFoundError: ProviderModelNotFoundError": 1
},
"noProgressIterations": 1,
"shortIterations": 1
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"active": true,
"iteration": 1,
"minIterations": 1,
"maxIterations": 0,
"completionPromise": "COMPLETE",
"tasksMode": false,
"taskPromise": "READY_FOR_NEXT_TASK",
"prompt": "# Ralph Wiggum Loop - Iteration Prompt\n\nYou are operating inside an automated loop. Each iteration you receive fresh context - you have NO memory of previous iterations. Your only persistence is the filesystem.\n\nYou are converting the completed `concept.html` (ECG Heartbeat CV Website) into a modern React application with TypeScript, Vite, and Tailwind CSS. The goal is a portfolio-grade React implementation that preserves all animations, interactions, and design details from the HTML concept.\n\n## Your Task This Iteration\n\n1. **Use the /frontend-design skill** (REQUIRED for visual components): Before writing ANY code for components that involve visual design, styling, animations, or UI elements, you MUST invoke the `/frontend-design` skill. This includes: BootSequence, ECGAnimation, FloatingNav, Hero, Skills, Experience, Education, Projects, Contact, Footer, and any component with CSS/styling. This skill gives you access to specialized frontend design capabilities for higher quality, polished output.\n\n2. **Read the plan**: Open `IMPLEMENTATION_PLAN.md` and find the highest-priority unchecked item (`- [ ]`). Items are listed in priority order - pick the first unchecked one.\n\n3. **Read accumulated learnings**: Open `progress.txt` and read the \"Codebase Patterns\" section. This contains learnings from previous iterations.\n\n4. **Read guardrails**: Open `guardrails.md` and read ALL guardrails. These are hard rules you MUST follow. Violating a guardrail is a quality check failure.\n\n5. **Implement the item**: Complete the single task you selected. Keep changes focused - one task per iteration. Write production-quality React/TypeScript code that is artistic, creative, and visually polished. This is a design showcase - the output should make someone say \"wow, that's slick.\"\n\n6. **Run quality checks**: Execute the quality check commands listed in `IMPLEMENTATION_PLAN.md` under \"Quality Checks\". Fix any issues before proceeding.\n\n7. **Commit your changes**: Stage and commit all changes with a descriptive message referencing the task you completed.\n\n8. **Mark the item complete**: In `IMPLEMENTATION_PLAN.md`, change the item from `- [ ]` to `- [x]`.\n\n9. **Update progress.txt**: Append to the \"Iteration Log\" section with:\n - Which task you completed\n - Any learnings or codebase patterns discovered (add to \"Codebase Patterns\" section)\n - Any issues encountered\n - Design decisions made (if visual component)\n\n10. **Commit the progress update**: Stage and commit the updated `IMPLEMENTATION_PLAN.md` and `progress.txt`.\n\n11. **Check for completion**: If ALL items in the task checklist are now checked (`- [x]`), output the following completion signal on its own line:\n\n```\n<promise>COMPLETE</promise>\n```\n\n## Critical Rules\n\n- **ALWAYS invoke /frontend-design skill before writing visual component code** — this is mandatory for BootSequence, ECGAnimation, FloatingNav, Hero, Skills, Experience, Education, Projects, Contact, Footer, and any styled component\n- **Only work on ONE task per iteration**\n- **Always read progress.txt AND guardrails.md before starting** — previous iterations may have left important context\n- **If a task is blocked or unclear**, document why in progress.txt and move to the next unchecked item\n- **Keep commits atomic and well-described**\n- **If quality checks fail, fix the issues before committing**\n- **The visual quality bar is HIGH** — this is a design portfolio piece\n- **Preserve all animations exactly** — timing, easing, and visual effects must match concept.html\n- **Use TypeScript strictly** — no `any` types, proper interfaces for all data structures\n- **Follow the established project structure** — components in `src/components/`, hooks in `src/hooks/`, etc.\n\n## Reference Files\n\n- `References/concept.html` — The complete working HTML implementation (your source of truth for animations, styling, timing)\n- `References/CV_v4.md` — CV content to populate sections\n- `References/ECGVideo/` — Remotion video project with ECG animation patterns\n",
"startedAt": "2026-02-10T15:15:39.896Z",
"model": "openrouter/pony-alpha",
"agent": "opencode"
}
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"read": "allow",
"edit": "allow",
"glob": "allow",
"grep": "allow",
"list": "allow",
"bash": "allow",
"task": "allow",
"webfetch": "allow",
"websearch": "allow",
"codesearch": "allow",
"todowrite": "allow",
"todoread": "allow",
"question": "allow",
"lsp": "allow",
"external_directory": "allow"
}
}
-294
View File
@@ -1,294 +0,0 @@
# Implementation Plan
## Project Overview
**ECG Heartbeat Traces Page Into Existence** - A single self-contained HTML file implementing an animated CV website for Andy Charlwood, MPharm. The page opens on a pure black screen with a blinking green terminal cursor. A boot sequence types out system diagnostic lines over ~4 seconds. After boot text fades, a green ECG flatline draws across the center of the screen. Three heartbeat pulses travel along the line with escalating amplitude. On the third and largest pulse, the ECG line "overflows" - branch lines shoot outward from the peak, tracing the outlines of page containers, card borders, and navigation into existence. The line color shifts from green to teal as the background rapidly transitions from black to white. SVG trace lines fade out and the final page content fades in. The final design is a modern medical device UI inspired by Apple Health and Withings, using Plus Jakarta Sans + Inter Tight fonts, white background, teal primary (#00897B), coral secondary (#FF6B6B), floating pill navigation bar, circular SVG skill gauges, and clean rounded cards.
## Quality Checks
- Open the HTML file in a browser and verify: boot sequence plays correctly, ECG flatline draws across screen, three heartbeats animate with increasing amplitude, branching lines trace outward on third beat, background transitions to white, final design renders all sections with medical device aesthetic, responsive at 768px and 480px, no console errors
- Verify all Google Fonts load correctly (Plus Jakarta Sans, Inter Tight, Fira Code)
- Verify all scroll animations trigger on scroll (sections fade in, skill gauges animate)
- Verify navigation links scroll to correct sections and floating pill nav tracks active section
- Verify SVG skill progress circles animate their stroke-dashoffset on scroll-reveal
- Verify decorative ECG waveforms render in section headers and footer
## Tasks
- [x] **Task 1: Build the boot screen foundation**
Create a single `index.html` file with all HTML, CSS, and JavaScript inline. The page starts as a pure black background (`#000`) filling the full viewport. Load Fira Code from Google Fonts (`https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap`). Create a boot screen container (`#boot-screen`) that is `position: fixed; inset: 0; background: #000; z-index: 1000; display: flex; flex-direction: column; justify-content: center; padding: 40px; font-family: 'Fira Code', monospace; font-size: 14px; overflow: hidden;`.
Display a blinking green cursor (a `span` with `display: inline-block; width: 8px; height: 16px; background: #00ff41; animation: blink 1s step-end infinite;`). The `@keyframes blink` toggles `opacity` between 1 and 0 at 50%.
Type out the following boot lines one by one with a ~300ms delay between lines. Each line element starts at `opacity: 0; transform: translateY(8px);` and animates to `opacity: 1; transform: translateY(0);` over 400ms ease-out. The cursor moves to the end of each new line as it appears.
Boot text lines (use `<span>` elements for color coding):
1. `> BIOS v3.1.4 ... loading` (dim grey `#666`)
2. `[OK] Kernel initialized` (`[OK]` in green `#00ff41`, rest in dim grey `#666`)
3. `[OK] Memory check: 8192MB` (`[OK]` in green, rest in dim grey)
4. `Loading modules...` (dim grey)
5. ` > pharmacist_core.sys` (cyan `#00bcd4`)
6. ` > nhs_interface.dll` (cyan)
7. ` > population_health.mod` (cyan)
8. ` > data_analytics.eng` (cyan)
9. `[OK] All systems operational` (`[OK]` in green, rest in dim grey)
10. `Initializing CV render pipeline...` (green `#00ff41`, bold)
11. `> READY` (bright green `#00ff41`, bold)
After the final line appears, wait 400ms, then remove the blinking cursor. The entire boot sequence should take approximately 4 seconds total. The boot screen sits on top of the final CV content (which is already in the DOM but hidden behind the boot screen).
- [x] **Task 2: Build the ECG flatline and first heartbeat**
After the boot sequence completes, fade the boot text to `opacity: 0` over `800ms`. Once faded, the black background remains. Create a full-viewport `<svg>` element overlaying the boot screen (`position: fixed; inset: 0; z-index: 1001;`).
**Flatline:** Draw a horizontal `<path>` at the vertical center of the viewport (`cy = window.innerHeight / 2`). The path is a straight line from `x=0` to `x=window.innerWidth`, `stroke: #00ff41; stroke-width: 2; fill: none;`. Add a green glow: `filter: drop-shadow(0 0 4px rgba(0, 255, 65, 0.6));`. Animate the flatline drawing left-to-right using `stroke-dasharray` set to the total path length and `stroke-dashoffset` transitioning from the total length to `0` over `1000ms` with linear timing.
**First heartbeat (PQRST complex):** After the flatline finishes drawing, a PQRST waveform travels across the center of the screen. Build this as a separate SVG `<path>` that traces a standard ECG waveform shape. The waveform shape relative to baseline (y = center):
- P wave: gentle upward bump, ~8px above baseline, ~30px wide
- Flat segment: ~10px
- Q dip: sharp dip ~10px below baseline, ~8px wide
- R spike: sharp peak ~40px above baseline (this is the first beat's amplitude), ~12px wide
- S dip: sharp dip ~15px below baseline, ~8px wide
- Flat segment: ~10px
- T wave: gentle upward bump, ~12px above baseline, ~35px wide
- Return to baseline
Position this waveform at the horizontal center of the screen. Animate it using `stroke-dasharray`/`stroke-dashoffset` to "draw" from left to right over ~600ms. The line color is `#00ff41` (green). The flatline behind the waveform remains visible. After this first beat, hold for 300ms.
- [x] **Task 3: Build second and third heartbeats with overflow branching**
**Second heartbeat:** Same PQRST shape but with larger amplitude: R peak at ~60px above baseline. Position it after a short flatline segment following the first beat. During this second beat, begin color-shifting the ECG line from green (`#00ff41`) toward teal (`#00897B`) by interpolating the stroke color. The page background begins lightening from `#000` toward `#0A0A0A`. Animate drawing over ~600ms. Hold 300ms.
**Third heartbeat:** Largest amplitude: R peak at ~100px above baseline. Full teal color (`#00897B`). Animated over ~600ms. Background lightens further.
**Overflow branching:** At the moment the third R peak reaches its apex, the ECG line "overflows." From the peak point, multiple SVG `<path>` branch lines shoot outward in different directions. Each branch traces the outline of a UI element on the final page:
- Branch 1: shoots upward and traces the outline of the floating pill nav bar (a rounded rectangle path at the top center of the viewport)
- Branch 2: shoots left and right, tracing the hero section container borders
- Branch 3-6: trace card borders, section containers, and other UI outlines
Each branch is a separate SVG `<path>` with its own `stroke-dasharray`/`stroke-dashoffset` animation. Branches are staggered by `50-100ms` each. All branches use teal stroke `#00897B`, `stroke-width: 1.5`, with glow `filter: drop-shadow(0 0 3px rgba(0, 137, 123, 0.5));`.
**Background transition:** During the branching (over ~800ms), the background rapidly transitions from near-black to white `#FFFFFF` using a CSS transition on the boot screen overlay's background-color.
**Fade out and reveal:** After all branches have finished drawing (~1.5s total branching time), fade all SVG lines to `opacity: 0` over `500ms`. Simultaneously fade in the final page content from `opacity: 0` to `opacity: 1`. Remove the SVG overlay and boot screen from the DOM.
- [x] **Task 4: Build final design skeleton - CSS variables, floating pill nav, typography**
Below the boot screen in the HTML, build the full CV page structure. Load Google Fonts: `Plus Jakarta Sans` (400, 500, 600, 700) and `Inter Tight` (400, 500, 600) alongside Fira Code. URL: `https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter+Tight:wght@400;500;600&display=swap`.
Define CSS custom properties on `:root`:
```
--bg: #FFFFFF;
--text: #334155;
--heading: #0F172A;
--teal: #00897B;
--teal-light: rgba(0, 137, 123, 0.08);
--teal-medium: rgba(0, 137, 123, 0.15);
--coral: #FF6B6B;
--coral-light: rgba(255, 107, 107, 0.08);
--muted: #94A3B8;
--border: #E2E8F0;
--card-bg: #FFFFFF;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.1);
--radius: 16px;
--font-primary: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-secondary: 'Inter Tight', system-ui, sans-serif;
```
Set `body` to `background: var(--bg); color: var(--text); font-family: var(--font-primary); font-size: 15px; line-height: 1.7; margin: 0; -webkit-font-smoothing: antialiased;`.
**Floating pill navigation bar:** A `<nav>` element styled as a floating pill: `position: fixed; top: 16px; left: 50%; transform: translateX(-50%); z-index: 100; max-width: 600px; width: auto; background: var(--card-bg); border-radius: 999px; padding: 8px 24px; box-shadow: var(--shadow-md); display: flex; align-items: center; gap: 4px;` (NOT full width - it floats as a contained pill shape in the center top of the page).
Nav links: `font-family: var(--font-secondary); font-size: 13px; font-weight: 500; color: var(--muted); text-decoration: none; padding: 6px 14px; border-radius: 999px; transition: all 0.3s ease;`. Hover: `color: var(--teal); background: var(--teal-light);`. Active state (tracked by IntersectionObserver on sections): `color: var(--teal); font-weight: 600;` with a small teal dot below: `::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 4px; height: 4px; border-radius: 50%; background: var(--teal); }`.
Sections in the nav: About, Skills, Experience, Education, Projects, Contact.
**Active section tracking:** Use IntersectionObserver on each `<section>` element with `{ threshold: 0.3, rootMargin: '-20% 0px -60% 0px' }`. When a section enters, add `.active` class to the corresponding nav link and remove from all others.
**Main container:** `<main>` with `max-width: 1000px; margin: 0 auto; padding: 0 32px;`. Sections have `padding: 80px 0;`.
- [x] **Task 5: Build hero section with vital sign cards**
Hero section (`#about`): `min-height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center;`.
1. **Name:** `<h1>` with `font-family: var(--font-primary); font-weight: 700; font-size: clamp(36px, 5vw, 52px); color: var(--heading); line-height: 1.2; margin: 0;`. Text: "Andy Charlwood".
2. **Title:** `<p>` with `font-size: 16px; color: var(--muted); margin: 8px 0;`. Text: "Deputy Head of Population Health & Data Analysis".
3. **Location pill:** `<span>` with `display: inline-block; padding: 4px 16px; border: 1px solid var(--teal); border-radius: 999px; font-size: 12px; color: var(--teal); font-weight: 500;`. Text: "Norwich, UK".
4. **Summary:** `<p>` with `font-size: 15px; line-height: 1.8; max-width: 560px; color: var(--text); margin: 24px auto 0; text-align: center;`. Text: "GPhC Registered Pharmacist specialising in medicines optimisation, population health analytics, and NHS efficiency programmes. Bridging clinical pharmacy with data science to drive meaningful improvements in patient outcomes."
5. **Vital sign metric cards:** A row of 4 cards below the summary. Container: `display: flex; gap: 16px; margin-top: 40px; justify-content: center; flex-wrap: wrap;`. Each card: `background: var(--card-bg); border-radius: var(--radius); padding: 20px 24px; box-shadow: var(--shadow-sm); border-top: 3px solid var(--teal); min-width: 160px; text-align: center;`.
Card content (each card has a large metric value and a label below):
- Card 1: Value "10+" in `font-size: 28px; font-weight: 700; color: var(--heading);`. Label "Years Experience" in `font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-top: 4px;`.
- Card 2: Value "Python/SQL/BI" (font-size 16px to fit). Label "Analytics Stack".
- Card 3: Value "Pop. Health" (font-size 18px). Label "Focus Area".
- Card 4: Value "NHS N&W" (font-size 18px). Label "System".
- [x] **Task 6: Build skills section with circular SVG progress gauges**
Skills section (`#skills`). Section heading "Skills & Expertise" in `font-size: 24px; font-weight: 700; color: var(--heading); margin-bottom: 32px; text-align: center;`.
CSS grid layout: `display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 24px;`.
Each skill item is a centered card: `display: flex; flex-direction: column; align-items: center; padding: 16px;`.
**SVG circular progress gauge per skill:** Each gauge is an `<svg>` with `width: 80; height: 80; viewBox: 0 0 80 80;`. Contains:
1. Background circle: `<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>`.
2. Progress circle: `<circle cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>`. The circumference is `2 * PI * 34 = ~213.6`. Set `stroke-dasharray: 213.6;` and `stroke-dashoffset: 213.6;` (fully hidden initially). On scroll-reveal, animate `stroke-dashoffset` to `213.6 * (1 - percentage/100)` over `1.2s ease-out`. Clinical skills use `stroke="var(--coral)"` instead of teal.
3. Center text: `<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)">` showing the percentage value.
Below the SVG: skill name in `font-size: 12px; font-weight: 600; color: var(--heading); margin-top: 8px;` and category in `font-size: 10px; color: var(--muted); text-transform: uppercase;`.
Skills with approximate proficiency percentages (for visual gauge display):
- **Technical (teal):** Python (90%), SQL (88%), Power BI (92%), JavaScript/TypeScript (70%), Data Analysis (95%), Dashboard Dev (88%), Algorithm Design (82%), Data Pipelines (80%)
- **Clinical (coral):** Medicines Optimisation (95%), Pop. Health Analytics (90%), NICE TA (85%), Health Economics (80%), Clinical Pathways (82%), CD Assurance (88%)
- **Strategic (teal):** Budget Mgmt (90%), Stakeholder Engagement (88%), Pharma Negotiation (85%), Team Development (82%)
**Scroll-triggered animation:** Use IntersectionObserver on the skills section. When it enters the viewport (threshold 0.15), animate each skill gauge's `stroke-dashoffset` to its target value. Stagger each gauge by `100ms` (first gauge starts immediately, second at 100ms, third at 200ms, etc.).
- [x] **Task 7: Build experience section with timeline and ECG decoration**
Experience section (`#experience`). Section heading "Experience" in `font-size: 24px; font-weight: 700; color: var(--heading);`.
**Decorative ECG waveform:** Next to the section heading, include a small inline SVG (~200px wide, ~30px tall) showing a simplified PQRST waveform in `stroke: var(--teal); opacity: 0.3; stroke-width: 1.5; fill: none;`. This is purely decorative.
**Timeline layout:** `position: relative;` container with a vertical line: `::before { content: ''; position: absolute; left: 20%; top: 0; bottom: 0; width: 2px; background: var(--teal); opacity: 0.2; }` (positioned at 20% from left).
Each timeline entry: `position: relative; padding-left: calc(20% + 32px); margin-bottom: 32px;`. Each has a timeline dot: `position: absolute; left: 20%; top: 8px; transform: translateX(-50%); width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--teal); background: var(--bg);`. Current role: `background: var(--teal);` (filled).
Each entry is a card: `background: var(--card-bg); border-radius: var(--radius); padding: 24px; box-shadow: var(--shadow-sm); border-left: 3px solid transparent; transition: all 0.3s ease;`. Hover: `box-shadow: var(--shadow-md); transform: scale(1.01); border-left-color: rgba(0, 137, 123, 0.3);`.
Inside each card:
- Role: `font-size: 17px; font-weight: 600; color: var(--heading); margin: 0;`
- Org: `font-size: 14px; color: var(--teal); margin: 2px 0;`
- Date: displayed as a pill badge `display: inline-block; padding: 2px 10px; background: var(--teal-light); border-radius: 999px; font-size: 12px; color: var(--teal); font-weight: 500; margin: 6px 0 12px;`
- Bullet list: `list-style: none; padding: 0;`. Each `<li>` has a teal dot: `::before { content: ''; display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--teal); margin-right: 10px; vertical-align: middle; }`. Font: `font-size: 14px; line-height: 1.7; margin: 4px 0;`.
**Role 1 (filled dot):**
- Title: "Interim Head of Population Health & Data Analysis"
- Org: "NHS Norfolk & Waveney ICB"
- Date: "May 2025 - Nov 2025"
- Bullets:
- "Led team through organisational transition, maintaining delivery of £14.6M efficiency programme"
- "Directed strategic priorities for population health analytics across Norfolk & Waveney (population ~1M)"
- "Managed stakeholder relationships with system leaders, provider trusts, and primary care networks"
**Role 2 (filled dot):**
- Title: "Deputy Head of Population Health & Data Analysis"
- Org: "NHS Norfolk & Waveney ICB"
- Date: "Jul 2024 - Present"
- Bullets:
- "Deputised for Head of department across all operational and strategic functions"
- "Oversaw £220M medicines budget and led programme of cost improvement initiatives"
- "Developed Python-based switching algorithm processing 14,000 patients, delivering £2.6M savings"
- "Built Blueteq automation system reducing processing time by 70%, saving 200+ hours annually"
- "Created PharMetrics dashboard platform for real-time medicines expenditure tracking"
**Role 3:**
- Title: "High-Cost Drugs & Interface Pharmacist"
- Org: "NHS Norfolk & Waveney ICB"
- Date: "May 2022 - Jul 2024"
- Bullets:
- "Managed high-cost drugs budget across acute and community settings"
- "Led NICE Technology Appraisal implementation and horizon scanning"
- "Developed health economic models for biosimilar switching programmes"
- "Built data pipelines for automated reporting of medicines expenditure"
**Role 4:**
- Title: "Pharmacy Manager"
- Org: "Tesco Pharmacy"
- Date: "Nov 2017 - May 2022"
- Bullets:
- "Managed community pharmacy delivering 3,000+ items monthly"
- "Pioneered asthma screening service generating £1M+ national revenue"
- "Led team of 6 through COVID-19 pandemic service delivery"
- "Completed Mary Seacole NHS Leadership Programme (2018)"
**Role 5:**
- Title: "Duty Pharmacy Manager"
- Org: "Tesco Pharmacy"
- Date: "Aug 2016 - Nov 2017"
- Bullets:
- "Supported pharmacy manager in daily operations and clinical services"
- "Delivered Medicines Use Reviews and New Medicine Service consultations"
- "Maintained controlled drug compliance and clinical governance standards"
- [x] **Task 8: Build education, projects, contact, and footer sections**
**Education section** (`#education`):
Section heading "Education" in `font-size: 24px; font-weight: 700; color: var(--heading);`. 2-column CSS grid: `grid-template-columns: repeat(2, 1fr); gap: 20px;`.
Each education card: `background: var(--card-bg); border-radius: var(--radius); padding: 24px; box-shadow: var(--shadow-sm); border-top: 3px solid; border-image: linear-gradient(to right, var(--teal), var(--coral)) 1;`. Use a pseudo-element approach for the gradient top border with border-radius: apply `overflow: hidden;` on the card, and use a `::before` pseudo-element (`content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(to right, var(--teal), var(--coral));`).
Card 1: Degree "MPharm (Hons) Pharmacy" in `font-size: 17px; font-weight: 600; color: var(--heading);`. Institution "University of East Anglia" in `14px; color: var(--teal);`. Period "2011 -- 2015" in `13px; color: var(--muted);`. Classification "Upper Second-Class Honours (2:1)" in `14px;`.
Card 2: "Mary Seacole Leadership Programme" in 17px 600 heading. "NHS Leadership Academy" in teal. "2018" in muted. "National healthcare leadership development programme."
A-Levels: Below cards, `font-size: 13px; color: var(--muted);` reading "A-Levels: Mathematics (A*), Chemistry (B), Politics (C)".
**Projects section** (`#projects`):
Section heading "Projects". 2x2 CSS grid: `grid-template-columns: repeat(2, 1fr); gap: 20px;`. Each card: `background: var(--card-bg); border-radius: var(--radius); padding: 24px; box-shadow: var(--shadow-sm); position: relative; overflow: hidden; transition: all 0.3s ease;`. Hover effect: gradient border using a pseudo-element trick - `::before { content: ''; position: absolute; inset: 0; border-radius: var(--radius); padding: 2px; background: linear-gradient(135deg, var(--teal), var(--coral)); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask-composite: exclude; opacity: 0; transition: opacity 0.3s; }`. On hover: `::before { opacity: 1; }` and card gets `transform: translateY(-2px); box-shadow: var(--shadow-md);`.
Project 1: Title "PharMetrics" in `16px; font-weight: 600; color: var(--heading);`. Description: "Real-time medicines expenditure dashboard providing actionable analytics for NHS decision-makers." Link button: `display: inline-block; padding: 6px 16px; background: var(--teal); color: white; border-radius: 999px; font-size: 12px; font-weight: 500; text-decoration: none; margin-top: 12px;`. URL: `https://medicines.charlwood.xyz/`. Label: "Visit Project".
Project 2: "Patient Pathway Analysis". Description: "Data-driven analysis of patient pathways to identify optimisation opportunities and improve clinical outcomes."
Project 3: "Blueteq Generator". Description: "Automation tool reducing high-cost drug approval processing time by 70%, saving 200+ hours annually."
Project 4: "NMS Video". Description: "Educational video resource supporting New Medicine Service consultations, improving patient engagement."
**Contact section** (`#contact`):
Section heading "Contact". 4-column CSS grid: `grid-template-columns: repeat(4, 1fr); gap: 16px;`. Each item centered: `text-align: center;`. Icon circle: `width: 40px; height: 40px; border-radius: 50%; background: var(--teal-light); display: flex; align-items: center; justify-content: center; margin: 0 auto 8px; color: var(--teal); font-size: 18px;`. Use unicode symbols for icons.
Items:
- Phone icon (unicode `\u260E`), value "07795553088" in `font-size: 13px; color: var(--heading);`
- Email icon (`\u2709`), value "andy@charlwood.xyz" as link in teal
- LinkedIn icon (`\u2197`), value "linkedin.com/in/andrewcharlwood" as link in teal
- Location icon (`\u25CB`), value "Norwich, UK"
**Footer:**
`<footer>` with `text-align: center; padding: 48px 0 32px; border-top: 1px solid var(--border);`. Contains a small decorative inline SVG ECG trace: a flatline (~120px wide) with a single small PQRST pulse in the center, `stroke: var(--teal); opacity: 0.3; stroke-width: 1.5; fill: none; height: 20px;`. Below: `font-size: 12px; color: var(--muted);` reading "Andy Charlwood -- MPharm, GPhC Registered Pharmacist".
- [x] **Task 9: Implement scroll animations and responsive design**
**Scroll animations:**
Every `<section>` starts with `opacity: 0; transform: translateY(24px); transition: opacity 0.6s ease, transform 0.6s ease;`. On reveal: `opacity: 1; transform: translateY(0);`.
Use IntersectionObserver with `{ threshold: 0.15 }`. Stagger child cards/items by `50-100ms` using `transition-delay`.
**Skill gauge animation:** Handled in Task 6 - IntersectionObserver on `#skills` triggers `stroke-dashoffset` animation on each circular gauge, staggered by 100ms.
**Active nav tracking:** IntersectionObserver updates the `.active` class on nav links as sections scroll into view (implemented in Task 4).
**Smooth scrolling:** Nav links prevent default and scroll to target section: `window.scrollTo({ top: sectionElement.offsetTop - 70, behavior: 'smooth' });`.
**Responsive design at `max-width: 768px`:**
- Pill nav: `max-width: 100%; width: calc(100% - 32px); overflow-x: auto; padding: 6px 12px;`. Links become horizontally scrollable.
- Hero vital sign cards: `grid-template-columns: repeat(2, 1fr);` (2x2 grid instead of 4 in a row).
- Skills grid: `grid-template-columns: repeat(3, 1fr);` (3 columns).
- Experience timeline: `padding-left: 0;`. Timeline line and dots hidden. Cards become full-width stacked.
- Projects grid: `grid-template-columns: 1fr;` (single column).
- Contact grid: `grid-template-columns: repeat(2, 1fr);` (2x2).
- Main padding: `0 20px;`.
**Responsive design at `max-width: 480px`:**
- Pill nav: simplified. Show only a few key links or abbreviate. `font-size: 11px; padding: 4px 8px;` per link.
- Hero name: `28px;`. Vital sign cards: `grid-template-columns: 1fr;` (stacked).
- Skills grid: `grid-template-columns: repeat(2, 1fr);` (2 columns). SVG gauges size reduced to `width: 64; height: 64;`.
- Experience cards: reduced padding `16px;`.
- Education grid: `grid-template-columns: 1fr;`.
- Contact grid: `grid-template-columns: repeat(2, 1fr);`.
- Main padding: `0 16px;`.
- Section padding: `48px 0;`.
-46
View File
@@ -1,46 +0,0 @@
# Ralph Wiggum Loop - Iteration Prompt
You are operating inside an automated loop. Each iteration you receive fresh context - you have NO memory of previous iterations. Your only persistence is the filesystem.
You are building a single self-contained HTML file that implements a CV website concept. The file includes a boot screen animation, a unique transition effect, and a final clean healthcare-inspired design. All CSS goes in a `<style>` tag, all JS in a `<script>` tag wrapped in an IIFE with `'use strict'`.
## Your Task This Iteration
1. **Use the /frontend-design skill**: Before writing any code, invoke the `/frontend-design` skill. This gives you access to specialized frontend design capabilities that will produce higher quality, more polished output. You MUST use this skill every iteration.
2. **Read the plan**: Open `IMPLEMENTATION_PLAN.md` and find the highest-priority unchecked item (`- [ ]`). Items are listed in priority order - pick the first unchecked one.
3. **Read accumulated learnings**: Open `progress.txt` and read the "Codebase Patterns" section. This contains learnings from previous iterations that will help you avoid mistakes.
4. **Read guardrails**: Open `guardrails.md` and read ALL guardrails (both standard and project-specific). These are hard rules you MUST follow. Violating a guardrail is a quality check failure.
5. **Implement the item**: Complete the single task you selected. Keep changes focused - one task per iteration. Write production-quality HTML/CSS/JS that is artistic, creative, and visually polished. This is a design showcase - the output should make someone say "wow, that's slick."
6. **Run quality checks**: Execute the quality check commands listed in `IMPLEMENTATION_PLAN.md` under "Quality Checks". Fix any issues before proceeding.
7. **Commit your changes**: Stage and commit all changes with a descriptive message referencing the task you completed.
8. **Mark the item complete**: In `IMPLEMENTATION_PLAN.md`, change the item from `- [ ]` to `- [x]`.
9. **Update progress.txt**: Append to the "Iteration Log" section with:
- Which task you completed
- Any learnings or codebase patterns discovered (also add these to the "Codebase Patterns" section if they'd help future iterations)
- Any issues encountered
10. **Commit the progress update**: Stage and commit the updated `IMPLEMENTATION_PLAN.md` and `progress.txt`.
11. **Check for completion**: If ALL items in the task checklist are now checked (`- [x]`), output the following completion signal on its own line:
```
<promise>COMPLETE</promise>
```
## Rules
- **ALWAYS invoke /frontend-design before writing any code** - this is critical for design quality
- Only work on ONE task per iteration
- Always read progress.txt AND guardrails.md before starting - previous iterations may have left important context
- If a task is blocked or unclear, document why in progress.txt and move to the next unchecked item
- Keep commits atomic and well-described
- If quality checks fail, fix the issues before committing
- The visual quality bar is HIGH - this is a design portfolio piece, not a functional prototype
-64
View File
@@ -1,64 +0,0 @@
# Ralph Wiggum Loop - Generic Software Template
A PowerShell outer loop that repeatedly invokes Claude Code with fresh context each iteration. Memory persists via filesystem only: git commits, `progress.txt`, and `IMPLEMENTATION_PLAN.md`.
## How It Works
1. `ralph.ps1` pipes `RALPH_PROMPT.md` to `claude --print` in a loop
2. Each iteration, Claude reads `IMPLEMENTATION_PLAN.md`, picks the first unchecked task, implements it, commits, and updates `progress.txt`
3. When all tasks are checked off, Claude outputs `<promise>COMPLETE</promise>` and the loop exits
4. No context accumulates between iterations - each one starts clean
## Setup
1. Copy this template folder into your project directory
2. Edit `IMPLEMENTATION_PLAN.md`:
- Fill in the **Project Overview** section
- Add your **Quality Checks** commands (e.g. `npm test`, `dotnet build`)
- List your **Tasks** as `- [ ]` checklist items in priority order
3. Ensure `claude` CLI is available on your PATH
## Usage
Basic run:
```powershell
.\ralph.ps1
```
With options:
```powershell
.\ralph.ps1 -MaxIterations 15 -Model sonnet -BranchName "feature/my-feature"
```
### Parameters
| Parameter | Default | Description |
|---|---|---|
| `-MaxIterations` | 10 | Maximum loop iterations before stopping |
| `-Model` | sonnet | Claude model to use |
| `-BranchName` | *(none)* | Git branch to create/checkout before starting |
### Exit Codes
| Code | Meaning |
|---|---|
| 0 | All tasks completed |
| 1 | Max iterations reached without completing all tasks |
## Files
| File | Purpose |
|---|---|
| `ralph.ps1` | PowerShell outer loop script |
| `RALPH_PROMPT.md` | Prompt template piped to Claude each iteration |
| `IMPLEMENTATION_PLAN.md` | Task checklist and project config (you edit this) |
| `progress.txt` | Accumulated learnings and iteration log (auto-populated) |
## Git Behaviour
- The script initialises a git repo if one doesn't exist
- Creates/checks out `-BranchName` if provided
- Pushes to remote after each iteration (silently skips if no remote configured)
- Claude commits after implementing each task and after updating progress files
+137
View File
@@ -0,0 +1,137 @@
# Implementation Plan — React Conversion
## Project Overview
Convert the completed `concept.html` (ECG Heartbeat CV Website) into a modern React application with TypeScript, Vite, and Tailwind CSS. The project will be a portfolio-grade React implementation that preserves all animations, interactions, and design details from the HTML concept while following React best practices.
**Key Features to Port:**
- Boot sequence with terminal typing animation
- ECG flatline and heartbeat SVG animations
- Branching lines that trace UI elements into existence
- Color transition from green ECG to teal/coral design system
- Floating pill navigation with active section tracking
- SVG circular skill gauges with scroll-triggered animations
- Experience timeline with ECG decoration
- Scroll-reveal animations using IntersectionObserver
- Fully responsive design (desktop/tablet/mobile)
**Tech Stack:**
- React 18+ with TypeScript
- Vite for build tooling
- Tailwind CSS for styling
- Framer Motion for complex animations (boot sequence, ECG transitions)
- React Intersection Observer for scroll-triggered animations
- Lucide React for icons (replacing unicode symbols)
**Project Structure:**
```
src/
├── components/
│ ├── BootSequence.tsx # Terminal typing animation
│ ├── ECGAnimation.tsx # Flatline, heartbeats, branching
│ ├── FloatingNav.tsx # Pill navigation with active tracking
│ ├── Hero.tsx # About section with vitals
│ ├── Skills.tsx # Skill gauges with SVG circles
│ ├── Experience.tsx # Timeline layout
│ ├── Education.tsx # Education cards
│ ├── Projects.tsx # Project cards with gradient borders
│ ├── Contact.tsx # Contact grid
│ └── Footer.tsx # Footer with ECG decoration
├── hooks/
│ ├── useScrollReveal.ts # IntersectionObserver for scroll animations
│ └── useActiveSection.ts # Track active nav section
├── lib/
│ └── utils.ts # Utility functions (skill gauge math)
├── types/
│ └── index.ts # TypeScript interfaces
├── App.tsx # Main app with boot/ECG/CV phases
├── main.tsx # Entry point
└── index.css # Tailwind + custom CSS variables
```
**Reference Materials:**
- `References/concept.html` — Complete working HTML implementation with all animations
- `References/CV_v4.md` — Source CV content to populate sections
- `References/ECGVideo/` — Remotion video project with ECG animation patterns
## Quality Checks
- `npm run dev` — Development server starts without errors
- `npm run build` — Production build completes without errors
- `npm run lint` — No ESLint errors
- `npm run typecheck` — No TypeScript errors
- Open `http://localhost:5173` and verify:
- Boot sequence plays exactly as in concept.html (terminal typing, 4 second duration)
- ECG flatline draws left-to-right
- Three heartbeats animate with increasing amplitude
- Branching lines trace outward on third beat
- Background transitions from black to white
- Final CV design renders with all sections
- Floating pill nav tracks active section on scroll
- Skill gauges animate when scrolled into view
- All hover effects work (card elevation, gradient borders)
- Responsive layouts work at 768px and 480px
- No console errors
## Tasks
- [ ] **Task 1: Initialize React project with Vite + TypeScript + Tailwind**
Run `npm create vite@latest . -- --template react-ts` to scaffold the project. Install dependencies: `npm install framer-motion lucide-react`. Initialize Tailwind: `npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p`. Configure `tailwind.config.js` with custom colors (teal #00897B, coral #FF6B6B, etc.). Set up `src/index.css` with Tailwind directives and CSS custom properties matching concept.html.
- [ ] **Task 2: Set up project structure and types**
Create the folder structure (`components/`, `hooks/`, `lib/`, `types/`). Define TypeScript interfaces in `types/index.ts` for: `Skill` (name, level, category, color), `Experience` (role, org, date, bullets), `Education` (degree, institution, period, detail), `Project` (title, description, link?). Create `lib/utils.ts` with helper function `calculateSkillOffset(level: number, radius: number): number` that returns `2 * Math.PI * radius * (1 - level / 100)`.
- [ ] **Task 3: Build BootSequence component**
Create `components/BootSequence.tsx`. Implement terminal typing animation using Framer Motion or CSS transitions. Display boot lines with correct colors (cyan labels, green values, dim text). Use exact boot text from concept.html: "CLINICAL TERMINAL v3.2.1", "Initialising pharmacist profile...", SYSTEM/USER/ROLE/LOCATION, module loading, [OK] lines, READY. Duration: ~4 seconds. Emit `onComplete` callback when finished. Styling: black background, Fira Code font.
- [ ] **Task 4: Build ECGAnimation component**
Create `components/ECGAnimation.tsx`. Port the ECG logic from concept.html:
- SVG flatline drawing left-to-right (1000ms)
- Three PQRST heartbeats with increasing amplitude (40px → 60px → 100px)
- Color interpolation: #00ff41#00C9A7#00897B
- Branching lines from third R peak tracing UI outlines (pill nav, hero, cards)
- Background transition from black to white
- Emit `onComplete` callback when animation finishes
Use Framer Motion for path drawing animations (pathLength).
- [ ] **Task 5: Build FloatingNav component**
Create `components/FloatingNav.tsx`. Floating pill navigation bar fixed at top center. Links: About, Skills, Experience, Education, Projects, Contact. Active link tracking via `useActiveSection` hook (IntersectionObserver). Smooth scroll to sections on click. Responsive: horizontal scroll on mobile. Styling: white bg, rounded-full, shadow-md, teal active state with dot indicator.
- [ ] **Task 6: Build Hero section component**
Create `components/Hero.tsx`. Port hero section from concept.html: centered layout, name (clamp 36-52px), job title (muted), location pill (teal border), summary paragraph (max-width 560px). Four vital sign metric cards in a row: "10+ Years Experience", "Python/SQL/BI Analytics Stack", "Pop. Health Focus Area", "NHS N&W System". Cards have teal border-top, hover elevation. Responsive: 2x2 grid on tablet, stacked on mobile.
- [ ] **Task 7: Build Skills section with SVG gauges**
Create `components/Skills.tsx`. Three skill categories: Technical (8 skills, teal), Clinical (6 skills, coral), Strategic (4 skills, teal). Each skill has circular SVG progress gauge using calculated stroke-dashoffset. Scroll-triggered animation: gauges fill when section enters viewport, staggered by 100ms. Port all 18 skills with correct percentages from concept.html.
- [ ] **Task 8: Build Experience section with timeline**
Create `components/Experience.tsx`. Vertical timeline with 5 roles: Interim Head (May-Nov 2025), Deputy Head (Jul 2024-Present), High-Cost Drugs (May 2022-Jul 2024), Pharmacy Manager (Nov 2017-May 2022), Duty Pharmacy Manager (Aug 2016-Nov 2017). Decorative ECG waveform SVG beside heading. Timeline dot filled for current roles. Cards with hover effect (scale, shadow, left border). Responsive: hide timeline line on mobile, stack cards.
- [ ] **Task 9: Build Education, Projects, Contact sections**
Create `components/Education.tsx`, `components/Projects.tsx`, `components/Contact.tsx`.
**Education:** 2-column grid. MPharm (Hons) UEA 2011-2015 (2:1). Mary Seacole Leadership Programme 2018. Gradient top border (teal→coral). A-Levels line below.
**Projects:** 2x2 grid. PharMetrics (with link), Patient Pathway Analysis, Blueteq Generator, NMS Video. Gradient border hover effect.
**Contact:** 4-column grid. Phone, Email, LinkedIn, Location. Use Lucide icons (Phone, Mail, Linkedin, MapPin). Responsive: 2x2 on mobile.
- [ ] **Task 10: Build Footer component and main App.tsx**
Create `components/Footer.tsx`. Decorative ECG waveform SVG, attribution text. Update `App.tsx` to orchestrate the three phases: 1) BootSequence (4s), 2) ECGAnimation (4s), 3) CV Content (with all sections). Use React state to track current phase. Ensure smooth transitions between phases.
- [ ] **Task 11: Implement scroll animations and responsive design**
Create `hooks/useScrollReveal.ts`. IntersectionObserver-based hook for scroll-triggered section reveals. Add scroll-reveal animations to all sections (opacity 0→1, translateY 24px→0). Ensure animations only trigger once. Add responsive breakpoints: tablet (768px), mobile (480px). Test all layouts.
- [ ] **Task 12: Final integration, testing, and polish**
Run all quality checks. Verify TypeScript compiles without errors. Verify no console errors. Test boot sequence timing matches concept.html (~4s). Test ECG animation timing and easing. Verify all CV content accuracy against CV_v4.md. Test all interactive elements (nav, hover effects, scroll animations). Verify responsive layouts at all breakpoints. Final build test.
+56
View File
@@ -0,0 +1,56 @@
# Ralph Wiggum Loop - Iteration Prompt
You are operating inside an automated loop. Each iteration you receive fresh context - you have NO memory of previous iterations. Your only persistence is the filesystem.
You are converting the completed `concept.html` (ECG Heartbeat CV Website) into a modern React application with TypeScript, Vite, and Tailwind CSS. The goal is a portfolio-grade React implementation that preserves all animations, interactions, and design details from the HTML concept.
## Your Task This Iteration
1. **Use the /frontend-design skill** (REQUIRED for visual components): Before writing ANY code for components that involve visual design, styling, animations, or UI elements, you MUST invoke the `/frontend-design` skill. This includes: BootSequence, ECGAnimation, FloatingNav, Hero, Skills, Experience, Education, Projects, Contact, Footer, and any component with CSS/styling. This skill gives you access to specialized frontend design capabilities for higher quality, polished output.
2. **Read the plan**: Open `IMPLEMENTATION_PLAN.md` and find the highest-priority unchecked item (`- [ ]`). Items are listed in priority order - pick the first unchecked one.
3. **Read accumulated learnings**: Open `progress.txt` and read the "Codebase Patterns" section. This contains learnings from previous iterations.
4. **Read guardrails**: Open `guardrails.md` and read ALL guardrails. These are hard rules you MUST follow. Violating a guardrail is a quality check failure.
5. **Implement the item**: Complete the single task you selected. Keep changes focused - one task per iteration. Write production-quality React/TypeScript code that is artistic, creative, and visually polished. This is a design showcase - the output should make someone say "wow, that's slick."
6. **Run quality checks**: Execute the quality check commands listed in `IMPLEMENTATION_PLAN.md` under "Quality Checks". Fix any issues before proceeding.
7. **Commit your changes**: Stage and commit all changes with a descriptive message referencing the task you completed.
8. **Mark the item complete**: In `IMPLEMENTATION_PLAN.md`, change the item from `- [ ]` to `- [x]`.
9. **Update progress.txt**: Append to the "Iteration Log" section with:
- Which task you completed
- Any learnings or codebase patterns discovered (add to "Codebase Patterns" section)
- Any issues encountered
- Design decisions made (if visual component)
10. **Commit the progress update**: Stage and commit the updated `IMPLEMENTATION_PLAN.md` and `progress.txt`.
11. **Check for completion**: If ALL items in the task checklist are now checked (`- [x]`), output the following completion signal on its own line:
```
<promise>COMPLETE</promise>
```
## Critical Rules
- **ALWAYS invoke /frontend-design skill before writing visual component code** — this is mandatory for BootSequence, ECGAnimation, FloatingNav, Hero, Skills, Experience, Education, Projects, Contact, Footer, and any styled component
- **Only work on ONE task per iteration**
- **Always read progress.txt AND guardrails.md before starting** — previous iterations may have left important context
- **If a task is blocked or unclear**, document why in progress.txt and move to the next unchecked item
- **Keep commits atomic and well-described**
- **If quality checks fail, fix the issues before committing**
- **The visual quality bar is HIGH** — this is a design portfolio piece
- **Preserve all animations exactly** — timing, easing, and visual effects must match concept.html
- **Use TypeScript strictly** — no `any` types, proper interfaces for all data structures
- **Follow the established project structure** — components in `src/components/`, hooks in `src/hooks/`, etc.
## Reference Files
- `References/concept.html` — The complete working HTML implementation (your source of truth for animations, styling, timing)
- `References/CV_v4.md` — CV content to populate sections
- `References/ECGVideo/` — Remotion video project with ECG animation patterns
+90
View File
@@ -0,0 +1,90 @@
# Guardrails — React Conversion
## Standard Guardrails
### Frontend-design skill requirement
- **When**: Writing ANY component with visual styling, animations, or UI elements
- **Rule**: You MUST invoke the `/frontend-design` skill before writing code. This applies to: BootSequence, ECGAnimation, FloatingNav, Hero, Skills, Experience, Education, Projects, Contact, Footer, and any styled component.
- **Why**: The frontend-design skill provides specialized capabilities for creating polished, professional-grade visual output. Skipping it results in lower quality design.
### Boot sequence consistency
- **When**: Implementing BootSequence component
- **Rule**: Boot text must match concept.html exactly: "CLINICAL TERMINAL v3.2.1", "Initialising pharmacist profile...", SYSTEM/USER/ROLE/LOCATION labels with values, loading modules line, three [OK] lines, "---", and final ready line. Use Fira Code font, green #00ff41 for [OK] and values, cyan #00e5ff for labels, dim green #3a6b45 for other text.
- **Why**: Boot sequence is the shared identity across all concepts. Must be identical.
### ECG animation fidelity
- **When**: Implementing ECGAnimation component
- **Rule**: Timing and visual effects must match concept.html exactly: flatline 1000ms, three heartbeats with amplitudes 40px→60px→100px, color shift #00ff41#00C9A7#00897B, branching lines from third R peak, background transition black→white.
- **Why**: The ECG animation is the signature visual effect. Any deviation breaks the experience.
### CV content accuracy
- **When**: Adding CV content to components
- **Rule**: Use the expanded CV_v4.md content. Key roles in order: Interim Head (May-Nov 2025), Deputy Head (Jul 2024-Present), High-Cost Drugs & Interface Pharmacist (May 2022-Jul 2024), Pharmacy Manager Tesco (Nov 2017-May 2022), Duty Pharmacy Manager Tesco (Aug 2016-Nov 2017). Include Mary Seacole Programme in education. Include key achievements with specific numbers (£14.6M, 14,000 patients, £2.6M, 70%, 200hrs, £1M).
- **Why**: The CV data must be accurate and complete. Missing roles or wrong dates would be a critical error.
### TypeScript strictness
- **When**: Writing any TypeScript code
- **Rule**: No `any` types. Define interfaces for all data structures. Use proper React.FC types or function component signatures with typed props. Enable strict mode in tsconfig.json.
- **Why**: Type safety is a core benefit of the React conversion. `any` defeats the purpose.
### Google Fonts loading
- **When**: Setting up index.html
- **Rule**: Use preconnect links to fonts.googleapis.com AND fonts.gstatic.com (with crossorigin), then the font CSS link. Load ALL fonts: Fira Code, Plus Jakarta Sans, Inter Tight. Test that fonts actually render.
- **Why**: Fonts are critical to the design identity. Missing fonts break the visual concept.
### Transition timing
- **When**: Building the boot-to-design transition
- **Rule**: Boot phase should take ~4 seconds. ECG animation should take ~5-6 seconds. Total time from page load to fully revealed design: no more than 10 seconds.
- **Why**: Too long and users will leave. Too short and the effect is lost.
### No console errors
- **When**: Writing JavaScript/TypeScript
- **Rule**: No errors in the browser console. Handle edge cases: elements that might not exist, animation cleanup on unmount, proper dependency arrays in hooks.
- **Why**: Console errors suggest broken functionality and are a quality check failure.
### Responsive breakpoints
- **When**: Adding responsive CSS/Tailwind classes
- **Rule**: Must work at 3 breakpoints: desktop (>768px), tablet (<=768px), mobile (<=480px). Navigation must be usable at all sizes. Content must not overflow horizontally. Touch targets must be reasonable size.
- **Why**: CVs are often viewed on mobile devices.
### Scroll animation observer
- **When**: Implementing scroll-triggered animations
- **Rule**: Use IntersectionObserver via custom hook (useScrollReveal), NOT scroll event listeners. Set appropriate threshold (0.1-0.15). Animations should only play once (don't re-trigger on scroll up).
- **Why**: IntersectionObserver is more performant and reliable than scroll listeners.
### Tailwind CSS usage
- **When**: Writing component styles
- **Rule**: Use Tailwind utility classes for all styling. Only use inline styles or CSS modules for dynamic values that can't be expressed with Tailwind (e.g., stroke-dashoffset calculations). Extend Tailwind config for custom colors.
- **Why**: Consistent styling approach, smaller bundle size, better maintainability.
## Project-Specific Guardrails
### Framer Motion for complex animations
- **When**: Animating the boot sequence, ECG paths, branching lines
- **Rule**: Use Framer Motion's `motion` components and props (initial, animate, transition). Use `pathLength` for SVG drawing animations. Use `AnimatePresence` for exit animations. Define transition objects with exact timing from concept.html.
- **Why**: Framer Motion provides declarative, performant animations that are easier to maintain than imperative JS.
### Skill circle calculation
- **When**: Building SVG circular progress gauges in Skills component
- **Rule**: The circumference formula is `2 * Math.PI * radius`. `strokeDasharray = circumference`. `strokeDashoffset = circumference * (1 - level / 100)`. The circle MUST have `transform: rotate(-90deg)` to start progress from 12 o'clock position.
- **Why**: Wrong math or missing rotation produces circles that fill from the wrong position or have incorrect percentages.
### Component file structure
- **When**: Creating new components
- **Rule**: One component per file in `src/components/`. Named exports for components. Props interface defined at top of file. Follow naming: PascalCase for components (BootSequence.tsx), camelCase for hooks (useScrollReveal.ts).
- **Why**: Consistent organization makes the codebase maintainable.
### Lucide React icons
- **When**: Adding icons to Contact or other sections
- **Rule**: Use Lucide React icons instead of unicode symbols. Import specific icons: `import { Phone, Mail, Linkedin, MapPin } from 'lucide-react'`. Size icons consistently (default 24px or specified size prop).
- **Why**: Lucide provides consistent, scalable SVG icons that match the design system.
### Custom hooks for reusable logic
- **When**: Implementing scroll reveal, active section tracking
- **Rule**: Extract reusable logic into custom hooks in `src/hooks/`. Hooks should be composable and return values/functions needed by components. Name with `use` prefix.
- **Why**: Keeps components clean, enables reuse, follows React best practices.
### Vite configuration
- **When**: Setting up the project build
- **Rule**: Use Vite's default React template. Configure path aliases in `vite.config.ts` for clean imports (e.g., `@/components/Hero`). Ensure `build.outDir` is set correctly.
- **Why**: Vite provides fast dev server and optimized production builds.
+27
View File
@@ -0,0 +1,27 @@
# Progress Log — React Conversion Phase
## Codebase Patterns
- **Source of truth**: `References/concept.html` contains the complete working HTML implementation. All animations, timing, colors, and styling must be preserved exactly when porting to React.
- **Tech stack**: React 18+, TypeScript, Vite, Tailwind CSS, Framer Motion, Lucide React
- **Project structure**: Components in `src/components/`, hooks in `src/hooks/`, types in `src/types/`, utilities in `src/lib/`
- **Animation approach**: Framer Motion for complex sequences (boot, ECG), CSS transitions for simple hover effects, IntersectionObserver (via hook) for scroll-triggered animations
- **SVG animations**: Use Framer Motion's `pathLength` prop for drawing effects, or CSS `stroke-dasharray`/`stroke-dashoffset` for skill gauges
- **Skill gauge math**: `circumference = 2 * Math.PI * radius`, `strokeDashoffset = circumference * (1 - level / 100)`, rotate -90deg to start from top
- **Boot sequence timing**: 14 lines × 220ms = ~3080ms, plus 400ms pause, 800ms fade = ~4.28s total
- **ECG timing**: Flatline 1000ms + 3 beats × 600ms + holds 300ms + branching 1500ms + fade 500ms = ~5.5s
- **Color palette**:
- ECG phase: #000 (black), #00ff41 (green), #00e5ff (cyan), #3a6b45 (dim green)
- Final design: #00897B (teal), #FF6B6B (coral), #0F172A (heading), #334155 (text), #94A3B8 (muted)
- **Fonts**: Fira Code (boot), Plus Jakarta Sans (primary), Inter Tight (secondary)
- **Responsive breakpoints**: 768px (tablet), 480px (mobile)
## Iteration Log
### Phase Transition — React Conversion Setup
- Previous phase completed: Single HTML file `concept.html` fully built with all 9 tasks
- New phase started: Convert HTML concept to React + TypeScript + Vite project
- IMPLEMENTATION_PLAN.md updated with 12 React-specific tasks
- RALPH_PROMPT.md updated with explicit /frontend-design skill requirement for all visual components
- This progress.txt reset for new phase
<!-- Iterations will be logged here as tasks are completed -->
View File
+91
View File
@@ -0,0 +1,91 @@
# Andy Charlwood
**MPharm, GPhC Registered Pharmacist**
Norwich, UK • 07795553088 • andy@charlwood.xyz
---
## Profile
Healthcare leader combining clinical pharmacy expertise with proficiency in Python, SQL, and data analytics, self-taught over the past decade through a drive to find root causes in data and build the most efficient solutions to complex problems. Currently leading population health analytics for NHS Norfolk & Waveney ICB, serving a population of 1.2 million. Experienced in working with messy, real-world prescribing data at scale to deliver actionable insights—from financial scenario modelling and pharmaceutical rebate negotiation to algorithm design and population-level pathway development. Proven track record of identifying and prioritising efficiency programmes worth £14.6M+ through automated, data-driven analysis. Skilled at translating complex clinical, financial, and analytical requirements into clear recommendations for executive stakeholders.
---
## Core Competencies
**Technical:** Python • SQL • Power BI • JavaScript/TypeScript • Real-world data analysis • Dashboard and tool development • Algorithm design • Data pipeline development
**Healthcare Domain:** Medicines optimisation • Population health analytics • NICE technology appraisal implementation • Health economics and outcomes • Clinical pathway development • Controlled drug assurance
**Strategic & Leadership:** Budget management (£220M) • Stakeholder engagement • Pharmaceutical negotiation • Team development and training • Change management • Financial scenario modelling • Executive communication
---
## Professional Experience
### Interim Head, Population Health & Data Analysis
**NHS Norfolk & Waveney ICB** | MayNov 2025 | Norwich, England
Returned to substantive Deputy Head role following commencement of ICB-wide organisational consultation.
Led strategic delivery of population health initiatives and data-driven medicines optimisation across Norfolk & Waveney ICS, reporting to Associate Director of Pharmacy with presentation accountability to Chief Medical Officer and system-level programme boards.
- Identified and prioritised a £14.6M efficiency programme through comprehensive data analysis; achieved over-target performance by October 2025 through targeted, evidence-based interventions across the integrated care system.
- Built Python-based switching algorithm using real-world GP prescribing data to automatically identify patients on expensive drugs suitable for cost-effective alternatives—compressing months of manual analysis into 3 days, identifying 14,000 patients and £2.6M in annual savings, of which £2M is on target for delivery this financial year.
- Automated incentive scheme analysis, improving accuracy and targeting precision whilst enabling a novel GP payment system linking rewards to delivered savings; achieved 50% reduction in targeted prescribing within the first two months of deployment.
- Presented strategy, programme progress, and financial position to Chief Medical Officer on a bimonthly basis, providing evidence-based recommendations to inform executive decision-making.
- Led transformation from practice-level data to patient-level SQL analytics, enabling targeted interventions and a self-serve model for the wider team.
### Deputy Head, Population Health & Data Analysis
**NHS Norfolk & Waveney ICB** | Jul 2024Present | Norwich, England
Driving data analytics strategy for medicines optimisation, developing bespoke datasets and analytical frameworks from messy, real-world GP prescribing data to identify efficiency opportunities and address health inequalities across the integrated care system.
- Managed £220M prescribing budget with sophisticated forecasting models identifying cost pressures and enabling proactive financial planning.
- Collaborated with the ICB data engineering team to create a comprehensive medicines data table integrating all dm+d products with standardised strength calculations, morphine equivalent conversions, and Anticholinergic Burden scoring—providing a single source of truth for all medicines analytics across the system.
- Led financial scenario modelling for a system-wide DOAC switching programme, building an interactive dashboard incorporating rebate mechanics, clinician switching capacity, workforce constraints, and patent expiry timelines to quantify risk trade-offs for senior decision-makers.
- Led renegotiation of pharmaceutical rebate terms ahead of patent expiry, securing improved commercial position for the ICB.
- Supported commissioning of tirzepatide (NICE TA1026) including financial projections identifying eligible cohorts from real-world data; authored the initial executive paper advocating a primary care delivery model over a specialist provider on cost-effectiveness and accessibility grounds, driving the system's shift to a GP-led model following executive sign-off.
- Developed Python-based controlled drug monitoring system calculating oral morphine equivalents across all opioid prescriptions to track patient-level exposure over time, identifying high-risk patients and potential diversion—enabling previously impossible patient safety analysis at population scale.
- Educated colleagues on data interpretation and analytics best practices, improving data fluency across the team through training, documentation, and self-serve tools.
### High-Cost Drugs & Interface Pharmacist
**NHS Norfolk & Waveney ICB** | May 2022Jul 2024 | Norwich, England
Led implementation of NICE technology appraisals and high-cost drug pathways across the ICS. Wrote most of the system's high-cost drug pathways—spanning rheumatology, ophthalmology (wet AMD, DMO, RVO), dermatology, gastroenterology, neurology, and migraine—balancing legal requirements to implement TAs against financial costs and local clinical preferences. Engaged clinical leads across all sectors of care to agree pathways and secure system-wide adoption.
- Developed software automating Blueteq prior approval form creation: 70% reduction in required forms, 200 hours immediate savings, and ongoing 78 hours weekly efficiency gains.
- Integrated Blueteq data with secondary care activity databases, resolving critical data-matching limitations and enabling accurate high-cost drug to spend tracking.
- Created Python-based Sankey chart analysis tool visualising patient journeys through high-cost drug pathways, enabling trusts to audit compliance and identify improvement opportunities.
### Pharmacy Manager
**Tesco PLC** | Nov 2017May 2022 | Great Yarmouth, Norfolk
Managed all pharmacy operations with full autonomy across a 100-hour contract, leading regional KPI delivery initiatives and contributing to national operational improvements. Served as Local Pharmaceutical Committee representative for Norfolk.
- Identified and shared an asthma screening process that was adopted nationally across the Tesco pharmacy estate (~300 branches), reducing pharmacist time from approximately 60 hours to 6 hours per store per month and enabling the network to claim approximately £1M in revenue.
- Led creation of national induction training plan and eLearning modules for all new pharmacy staff, with enhanced focus on leadership development for non-pharmacist team members.
- Supervised two staff members through NVQ3 qualifications to pharmacy technician registration: full HR responsibilities including recruitment, performance management, and grievances.
---
## Education, Professional Development & Registration
**Master of Pharmacy (MPharm), 2:1 Honours** | University of East Anglia | 20112015
Research project on drug delivery and cocrystals: 75.1% (Distinction)
**NHS Leadership Academy Mary Seacole Programme** | 2018 | 78%
NHS leadership qualification: change management, healthcare leadership, system-level thinking
**A-Levels:** Mathematics (A\*), Chemistry (B), Politics (C) | Highworth Grammar School | 20092011
**GPhC Registered Pharmacist** | General Pharmaceutical Council | August 2016Present
---
*References available upon request.*
Submodule References/ECGVideo added at a5855fc226
+329 -331
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Andy Charlwood MPharm | CV</title> <title>Andy Charlwood — MPharm | CV</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter+Tight:wght@400;500;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter+Tight:wght@400;500;600&display=swap" rel="stylesheet">
@@ -117,37 +117,36 @@
pointer-events: none; pointer-events: none;
} }
#ecg-overlay svg { #ecg-overlay canvas {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: block;
} }
.ecg-line { .ecg-seed-dot {
fill: none; display: inline;
stroke: #00ff41; color: #00ff41;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 0 4px rgba(0, 255, 65, 0.6));
} }
.ecg-heartbeat { .ecg-seed-dot.glowing {
fill: none; text-shadow: 0 0 8px #00ff41, 0 0 16px #00ff41;
stroke: #00ff41; animation: seedPulse 0.6s ease-in-out infinite;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 0 6px rgba(0, 255, 65, 0.7));
} }
.ecg-branch { #travel-dot {
fill: none; position: fixed;
stroke: #00897B; z-index: 1002;
stroke-width: 1.5; width: 8px;
stroke-linecap: round; height: 8px;
stroke-linejoin: round; border-radius: 50%;
filter: drop-shadow(0 0 3px rgba(0, 137, 123, 0.5)); background: #00ff41;
box-shadow: 0 0 12px #00ff41, 0 0 24px rgba(0, 255, 65, 0.4);
pointer-events: none;
}
@keyframes seedPulse {
0%, 100% { text-shadow: 0 0 8px #00ff41, 0 0 16px #00ff41; }
50% { text-shadow: 0 0 14px #00ff41, 0 0 28px #00ff41, 0 0 40px rgba(0,255,65,0.3); }
} }
/* ========================================= /* =========================================
@@ -1381,7 +1380,7 @@
{ html: '<span class="c-green-bold">[OK]</span> <span class="c-dim">population_health.mod</span>', delay: 220 }, { html: '<span class="c-green-bold">[OK]</span> <span class="c-dim">population_health.mod</span>', delay: 220 },
{ html: '<span class="c-green-bold">[OK]</span> <span class="c-dim">data_analytics.eng</span>', delay: 220 }, { html: '<span class="c-green-bold">[OK]</span> <span class="c-dim">data_analytics.eng</span>', delay: 220 },
{ html: '<span class="c-dim">---</span>', delay: 220 }, { html: '<span class="c-dim">---</span>', delay: 220 },
{ html: '<span class="c-green-bold">&gt; READY Rendering CV...</span><span id="boot-cursor"></span>', delay: 220 } { html: '<span class="c-green-bold">&gt; READY — Rendering CV..<span class="ecg-seed-dot" id="ecg-seed-dot">.</span></span><span id="boot-cursor"></span>', delay: 220 }
]; ];
var bootContainer = document.getElementById('boot-lines'); var bootContainer = document.getElementById('boot-lines');
@@ -1399,14 +1398,35 @@
}, totalBootTime); }, totalBootTime);
}); });
// After all lines are visible, wait 400ms then remove cursor and fade boot text // After all lines visible, activate seed dot glow
var bootEndTime = totalBootTime + 400; var bootEndTime = totalBootTime + 400;
setTimeout(function() { setTimeout(function() {
// Remove cursor var seedDot = document.getElementById('ecg-seed-dot');
if (seedDot) seedDot.classList.add('glowing');
}, totalBootTime);
setTimeout(function() {
var cursor = document.getElementById('boot-cursor'); var cursor = document.getElementById('boot-cursor');
if (cursor) { if (cursor) cursor.remove();
cursor.remove();
// Capture seed dot position before fading
var seedDot = document.getElementById('ecg-seed-dot');
var seedRect = seedDot ? seedDot.getBoundingClientRect() : null;
// Create traveling dot at seed dot position
if (seedRect) {
var travelDot = document.createElement('div');
travelDot.id = 'travel-dot';
travelDot.style.left = (seedRect.left + seedRect.width / 2 - 4) + 'px';
travelDot.style.top = (seedRect.top + seedRect.height / 2 - 4) + 'px';
document.body.appendChild(travelDot);
requestAnimationFrame(function() {
travelDot.style.transition = 'all 700ms cubic-bezier(0.4, 0, 0.2, 1)';
travelDot.style.left = '-4px';
travelDot.style.top = (window.innerHeight / 2 - 4) + 'px';
});
} }
// Fade out all boot lines // Fade out all boot lines
@@ -1416,343 +1436,321 @@
} }
}, bootEndTime); }, bootEndTime);
// After fade out (800ms), start the ECG phase // After fade + dot travel, start ECG name animation
setTimeout(function() { setTimeout(function() {
startECGPhase(); var td = document.getElementById('travel-dot');
if (td) td.remove();
startECGNameAnimation();
}, bootEndTime + 800); }, bootEndTime + 800);
/* ========================================= /* =========================================
ECG PHASE: Flatline + First Heartbeat ECG NAME ANIMATION (ported from Remotion)
========================================= */ ========================================= */
function startECGPhase() { function generateHeartbeatPoints(amplitude) {
var points = [];
var steps = 200;
for (var i = 0; i <= steps; i++) {
var t = i / steps;
var y = 0;
if (t >= 0.05 && t < 0.2) { y = 0.12 * Math.sin(((t - 0.05) / 0.15) * Math.PI); }
else if (t >= 0.25 && t < 0.32) { y = -0.1 * Math.sin(((t - 0.25) / 0.07) * Math.PI); }
else if (t >= 0.32 && t < 0.42) { y = 1.0 * Math.sin(((t - 0.32) / 0.1) * Math.PI); }
else if (t >= 0.42 && t < 0.5) { y = -0.25 * Math.sin(((t - 0.42) / 0.08) * Math.PI); }
else if (t >= 0.55 && t < 0.75) { y = 0.2 * Math.sin(((t - 0.55) / 0.2) * Math.PI); }
points.push({ x: t, y: y * amplitude });
}
return points;
}
var ECG_LETTERS = {
A: [{x:0,y:0},{x:0.48,y:1},{x:0.53,y:0.42},{x:0.6,y:0.42},{x:1,y:0}],
N: [{x:0,y:0},{x:0.12,y:1},{x:0.72,y:0},{x:0.88,y:1},{x:1,y:0}],
D: [{x:0,y:0},{x:0.1,y:1},{x:0.5,y:1},{x:0.85,y:0.55},{x:1,y:0}],
R: [{x:0,y:0},{x:0.1,y:1},{x:0.35,y:1},{x:0.5,y:0.6},{x:0.55,y:0.45},{x:1,y:0}],
E: [{x:0,y:0},{x:0.1,y:1},{x:0.4,y:1},{x:0.45,y:0.5},{x:0.65,y:0.5},{x:0.7,y:0},{x:1,y:0}],
W: [{x:0,y:0},{x:0.05,y:1},{x:0.27,y:0},{x:0.5,y:0.65},{x:0.73,y:0},{x:0.95,y:1},{x:1,y:0}],
C: [{x:0,y:0},{x:0.08,y:0.6},{x:0.18,y:1},{x:0.6,y:1},{x:0.8,y:0.5},{x:0.95,y:0.1},{x:1,y:0}],
H: [{x:0,y:0},{x:0.1,y:1},{x:0.18,y:0.5},{x:0.82,y:0.5},{x:0.9,y:1},{x:1,y:0}],
L: [{x:0,y:0},{x:0.12,y:1},{x:0.3,y:1},{x:0.38,y:0},{x:1,y:0}],
O: [{x:0,y:0},{x:0.2,y:0.85},{x:0.35,y:1},{x:0.65,y:1},{x:0.8,y:0.85},{x:1,y:0}]
};
function interpolateLetterY(points, t) {
if (t <= points[0].x) return points[0].y;
if (t >= points[points.length - 1].x) return points[points.length - 1].y;
for (var i = 0; i < points.length - 1; i++) {
if (t >= points[i].x && t <= points[i + 1].x) {
var seg = (t - points[i].x) / (points[i + 1].x - points[i].x);
return points[i].y + (points[i + 1].y - points[i].y) * seg;
}
}
return 0;
}
var ECG_TEXT = 'ANDREW CHARLWOOD';
function ecgLayoutText(offsetX, lw, lg, sw) {
var layout = [];
var cursor = offsetX;
for (var i = 0; i < ECG_TEXT.length; i++) {
var ch = ECG_TEXT[i];
if (ch === ' ') { cursor += sw; continue; }
layout.push({ char: ch, startX: cursor, endX: cursor + lw, centerX: cursor + lw / 2 });
cursor += lw + lg;
}
return layout;
}
function ecgGetTextWidth(lw, lg, sw) {
var chars = ECG_TEXT.replace(/ /g, '').length;
var spaces = ECG_TEXT.split(' ').length - 1;
return chars * (lw + lg) - lg + spaces * sw;
}
function startECGNameAnimation() {
var overlay = document.getElementById('ecg-overlay'); var overlay = document.getElementById('ecg-overlay');
var bootScreen = document.getElementById('boot-screen'); var bootScreen = document.getElementById('boot-screen');
var vw = window.innerWidth; var vw = window.innerWidth;
var vh = window.innerHeight; var vh = window.innerHeight;
var cy = Math.round(vh / 2); var dpr = window.devicePixelRatio || 1;
// Create SVG var canvas = document.createElement('canvas');
var svgNS = 'http://www.w3.org/2000/svg'; canvas.width = vw * dpr;
var svg = document.createElementNS(svgNS, 'svg'); canvas.height = vh * dpr;
svg.setAttribute('viewBox', '0 0 ' + vw + ' ' + vh); overlay.appendChild(canvas);
svg.setAttribute('preserveAspectRatio', 'none'); var ctx = canvas.getContext('2d');
overlay.appendChild(svg); ctx.scale(dpr, dpr);
// --- FLATLINE --- // Responsive scale
var flatlinePath = document.createElementNS(svgNS, 'path'); var scale = Math.min(1.2, Math.max(0.35, vw / 1400));
var flatlineD = 'M 0 ' + cy + ' L ' + vw + ' ' + cy; var LETTER_W = 72 * scale;
flatlinePath.setAttribute('d', flatlineD); var LETTER_G = 10 * scale;
flatlinePath.setAttribute('class', 'ecg-line'); var SPACE_W = 30 * scale;
svg.appendChild(flatlinePath); var TRACE_SPEED = 450 * scale;
var FLAT_GAP = 0.4;
var HOLD_TIME = 0.75;
var EXIT_TIME = 0.8;
var baselineY = vh * 0.5;
var ecgMaxDefl = vh * 0.25;
var textMaxDefl = vh * 0.08;
var lineColor = '#00ff41';
var flatlineLen = flatlinePath.getTotalLength(); // Beats (time-based, widths scaled)
flatlinePath.style.strokeDasharray = flatlineLen; var beats = [
flatlinePath.style.strokeDashoffset = flatlineLen; { startTime: 0.5, widthPx: 60 * scale, amplitude: 0.3 },
{ startTime: 1.2, widthPx: 90 * scale, amplitude: 0.55 },
{ startTime: 2.0, widthPx: 120 * scale, amplitude: 0.85 },
{ startTime: 2.8, widthPx: 140 * scale, amplitude: 1.0 }
];
beats.forEach(function(b) { b.startWX = b.startTime * TRACE_SPEED; });
// Animate flatline drawing left-to-right over 1000ms var lastBeat = beats[beats.length - 1];
requestAnimationFrame(function() { var lastBeatEndWX = lastBeat.startWX + lastBeat.widthPx;
flatlinePath.style.transition = 'stroke-dashoffset 1000ms linear'; var textStartWX = lastBeatEndWX + FLAT_GAP * TRACE_SPEED;
flatlinePath.style.strokeDashoffset = '0'; var totalTextW = ecgGetTextWidth(LETTER_W, LETTER_G, SPACE_W);
}); var textEndWX = textStartWX + totalTextW;
var textLayout = ecgLayoutText(textStartWX, LETTER_W, LETTER_G, SPACE_W);
var fontSize = Math.round(textMaxDefl / 0.715);
// --- FIRST HEARTBEAT (R peak 40px, green) --- var headScreenRatio = 0.75;
setTimeout(function() { var finalHeadSX = (vw - totalTextW) / 2 + totalTextW;
drawHeartbeat(svg, svgNS, vw, vh, cy, 40, '#00ff41', 600, function() { var textEndTime = textEndWX / TRACE_SPEED;
// Hold 300ms then second heartbeat var holdEndTime = textEndTime + HOLD_TIME;
setTimeout(function() { var exitEndTime = holdEndTime + EXIT_TIME;
// --- SECOND HEARTBEAT (R peak 60px, transitioning color) --- function getYAtX(wx) {
// Bg begins lightening slightly for (var i = 0; i < beats.length; i++) {
bootScreen.style.transition = 'background 600ms ease'; var b = beats[i];
bootScreen.style.background = '#0A0A0A'; if (wx >= b.startWX && wx <= b.startWX + b.widthPx) {
var prog = (wx - b.startWX) / b.widthPx;
drawHeartbeat(svg, svgNS, vw, vh, cy, 60, '#00C9A7', 600, function() { var pts = generateHeartbeatPoints(b.amplitude);
// Hold 300ms then third heartbeat var idx = Math.min(Math.floor(prog * (pts.length - 1)), pts.length - 1);
setTimeout(function() { return baselineY - pts[idx].y * ecgMaxDefl;
}
// --- THIRD HEARTBEAT (R peak 100px, full teal) --- }
bootScreen.style.transition = 'background 600ms ease'; for (var j = 0; j < textLayout.length; j++) {
bootScreen.style.background = '#141414'; var item = textLayout[j];
if (wx >= item.startX && wx <= item.endX) {
drawHeartbeat(svg, svgNS, vw, vh, cy, 100, '#00897B', 600, function() { var t = (wx - item.startX) / (item.endX - item.startX);
// At the apex of the third beat — launch branching var ld = ECG_LETTERS[item.char];
startBranching(svg, svgNS, vw, vh, cy, overlay, bootScreen); if (ld) return baselineY - interpolateLetterY(ld, t) * textMaxDefl;
}); }
}, 300); }
}); return baselineY;
}, 300);
});
}, 1050);
}
/**
* Creates the overflow branching effect from the third heartbeat peak.
* Branch lines shoot outward and trace the outlines of UI elements.
*/
function startBranching(svg, svgNS, vw, vh, cy, overlay, bootScreen) {
var cx = Math.round(vw / 2);
var peakY = cy - 100; // Third beat R peak position
// Calculate UI element positions for branches to trace
// Pill nav: rounded rectangle at top center
var navW = Math.min(520, vw - 64);
var navH = 44;
var navX = Math.round((vw - navW) / 2);
var navY = 16;
var navR = 22; // border-radius
// Hero section: centered area
var heroW = Math.min(600, vw - 80);
var heroX = Math.round((vw - heroW) / 2);
var heroY = Math.round(vh * 0.2);
var heroH = Math.round(vh * 0.5);
// Card outlines (vital sign cards)
var cardW = 160;
var cardH = 80;
var cardGap = 16;
var totalCardsW = 4 * cardW + 3 * cardGap;
var cardsStartX = Math.round((vw - totalCardsW) / 2);
var cardsY = Math.round(vh * 0.65);
// Branch paths — each starts from the peak and traces outward
var branches = [];
// Branch 1: Shoots up to trace the pill nav bar outline
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + cx + ' ' + (navY + navH + 20) + ', ' + (navX + navR) + ' ' + (navY + navH) +
' L ' + (navX + navR) + ' ' + (navY + navH) +
' Q ' + navX + ' ' + (navY + navH) + ', ' + navX + ' ' + (navY + navH - navR) +
' L ' + navX + ' ' + (navY + navR) +
' Q ' + navX + ' ' + navY + ', ' + (navX + navR) + ' ' + navY +
' L ' + (navX + navW - navR) + ' ' + navY +
' Q ' + (navX + navW) + ' ' + navY + ', ' + (navX + navW) + ' ' + (navY + navR) +
' L ' + (navX + navW) + ' ' + (navY + navH - navR) +
' Q ' + (navX + navW) + ' ' + (navY + navH) + ', ' + (navX + navW - navR) + ' ' + (navY + navH) +
' L ' + (navX + navR) + ' ' + (navY + navH),
delay: 0
});
// Branch 2: Shoots left to trace the left edge of hero container
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (heroX + 40) + ' ' + (peakY - 30) + ', ' + heroX + ' ' + heroY +
' L ' + heroX + ' ' + (heroY + heroH),
delay: 80
});
// Branch 3: Shoots right to trace the right edge of hero container
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (heroX + heroW - 40) + ' ' + (peakY - 30) + ', ' + (heroX + heroW) + ' ' + heroY +
' L ' + (heroX + heroW) + ' ' + (heroY + heroH),
delay: 80
});
// Branch 4: Down-left to first card outline (trace rectangle, no Z back to peak)
var c1x = cardsStartX;
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (cx - 80) + ' ' + (cardsY - 40) + ', ' + c1x + ' ' + cardsY +
' L ' + (c1x + cardW) + ' ' + cardsY +
' L ' + (c1x + cardW) + ' ' + (cardsY + cardH) +
' L ' + c1x + ' ' + (cardsY + cardH) +
' L ' + c1x + ' ' + cardsY,
delay: 150
});
// Branch 5: Down to second card outline
var c2x = cardsStartX + cardW + cardGap;
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (c2x + cardW / 2) + ' ' + (cardsY - 20) + ', ' + c2x + ' ' + cardsY +
' L ' + (c2x + cardW) + ' ' + cardsY +
' L ' + (c2x + cardW) + ' ' + (cardsY + cardH) +
' L ' + c2x + ' ' + (cardsY + cardH) +
' L ' + c2x + ' ' + cardsY,
delay: 200
});
// Branch 6: Down-right to third card outline
var c3x = cardsStartX + 2 * (cardW + cardGap);
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (cx + 60) + ' ' + (cardsY - 30) + ', ' + c3x + ' ' + cardsY +
' L ' + (c3x + cardW) + ' ' + cardsY +
' L ' + (c3x + cardW) + ' ' + (cardsY + cardH) +
' L ' + c3x + ' ' + (cardsY + cardH) +
' L ' + c3x + ' ' + cardsY,
delay: 250
});
// Branch 7: Far right to fourth card outline
var c4x = cardsStartX + 3 * (cardW + cardGap);
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (cx + 120) + ' ' + (cardsY - 20) + ', ' + c4x + ' ' + cardsY +
' L ' + (c4x + cardW) + ' ' + cardsY +
' L ' + (c4x + cardW) + ' ' + (cardsY + cardH) +
' L ' + c4x + ' ' + (cardsY + cardH) +
' L ' + c4x + ' ' + cardsY,
delay: 300
});
// Animate each branch with staggered timing
var branchDuration = 800;
var maxDelay = 0;
branches.forEach(function(branch) {
if (branch.delay > maxDelay) maxDelay = branch.delay;
setTimeout(function() {
var path = document.createElementNS(svgNS, 'path');
path.setAttribute('d', branch.d);
path.setAttribute('class', 'ecg-branch');
svg.appendChild(path);
var len = path.getTotalLength();
path.style.strokeDasharray = len;
path.style.strokeDashoffset = len;
requestAnimationFrame(function() {
path.style.transition = 'stroke-dashoffset ' + branchDuration + 'ms cubic-bezier(0.25, 0.46, 0.45, 0.94)';
path.style.strokeDashoffset = '0';
});
}, branch.delay);
});
// Background rapidly transitions to white during branching
setTimeout(function() {
bootScreen.style.transition = 'background 800ms ease-out';
bootScreen.style.background = '#FFFFFF';
}, 200);
// After all branches finish drawing, fade out and reveal
var totalBranchTime = maxDelay + branchDuration + 100;
setTimeout(function() {
finishECGPhase(overlay, bootScreen);
}, totalBranchTime);
}
/**
* Draws a PQRST waveform centered horizontally on the screen.
* @param {SVGElement} svg - The SVG container
* @param {string} svgNS - SVG namespace
* @param {number} vw - Viewport width
* @param {number} vh - Viewport height
* @param {number} cy - Vertical center (baseline Y)
* @param {number} rHeight - Height of the R peak above baseline
* @param {string} color - Stroke color
* @param {number} duration - Animation duration in ms
* @param {function} onComplete - Callback when animation finishes
*/
function drawHeartbeat(svg, svgNS, vw, vh, cy, rHeight, color, duration, onComplete) {
// PQRST waveform shape relative to center point
// Total width of the waveform: ~160px
var waveWidth = 160;
var startX = Math.round((vw - waveWidth) / 2);
// Build the PQRST path
// P wave: gentle upward bump, ~8px above baseline, ~30px wide
// Flat: ~10px
// Q dip: sharp dip ~10px below, ~8px wide
// R spike: sharp peak rHeight above baseline, ~12px wide
// S dip: sharp dip ~15px below, ~8px wide
// Flat: ~10px
// T wave: gentle upward bump, ~12px above, ~35px wide
// Return to baseline
var x = startX;
var d = 'M ' + x + ' ' + cy;
// Lead-in flat segment
d += ' L ' + (x + 10) + ' ' + cy;
x += 10;
// P wave (cubic bezier for smooth bump)
d += ' C ' + (x + 8) + ' ' + cy + ', ' + (x + 10) + ' ' + (cy - 8) + ', ' + (x + 15) + ' ' + (cy - 8);
d += ' C ' + (x + 20) + ' ' + (cy - 8) + ', ' + (x + 22) + ' ' + cy + ', ' + (x + 30) + ' ' + cy;
x += 30;
// Flat segment before QRS
d += ' L ' + (x + 10) + ' ' + cy;
x += 10;
// Q dip
d += ' L ' + (x + 4) + ' ' + (cy + 10);
x += 4;
// R spike (sharp peak)
d += ' L ' + (x + 6) + ' ' + (cy - rHeight);
x += 6;
// S dip
d += ' L ' + (x + 6) + ' ' + (cy + 15);
x += 6;
// Return to baseline
d += ' L ' + (x + 4) + ' ' + cy;
x += 4;
// Flat segment before T wave
d += ' L ' + (x + 10) + ' ' + cy;
x += 10;
// T wave (cubic bezier for smooth bump)
d += ' C ' + (x + 8) + ' ' + cy + ', ' + (x + 12) + ' ' + (cy - 12) + ', ' + (x + 17) + ' ' + (cy - 12);
d += ' C ' + (x + 22) + ' ' + (cy - 12) + ', ' + (x + 27) + ' ' + cy + ', ' + (x + 35) + ' ' + cy;
x += 35;
// Trail-out flat segment
d += ' L ' + (x + 10) + ' ' + cy;
var beatPath = document.createElementNS(svgNS, 'path');
beatPath.setAttribute('d', d);
beatPath.setAttribute('class', 'ecg-heartbeat');
beatPath.style.stroke = color;
// Match the glow filter to the stroke color
if (color !== '#00ff41') {
beatPath.style.filter = 'drop-shadow(0 0 6px rgba(0, 137, 123, 0.7))';
} }
svg.appendChild(beatPath);
var beatLen = beatPath.getTotalLength(); var startTs = null;
beatPath.style.strokeDasharray = beatLen; var bgTransitioned = false;
beatPath.style.strokeDashoffset = beatLen;
// Animate the heartbeat drawing function animate(timestamp) {
requestAnimationFrame(function() { if (!startTs) startTs = timestamp;
beatPath.style.transition = 'stroke-dashoffset ' + duration + 'ms ease-in-out'; var elapsed = (timestamp - startTs) / 1000;
beatPath.style.strokeDashoffset = '0';
});
// Callback after animation if (elapsed >= exitEndTime) {
if (onComplete) { finishAnimation(overlay, bootScreen);
setTimeout(onComplete, duration + 50); return;
}
ctx.clearRect(0, 0, vw, vh);
var headWX = elapsed * TRACE_SPEED;
var isTextDone = elapsed >= textEndTime;
var isExitPhase = elapsed >= holdEndTime;
if (isExitPhase) {
headWX = textEndWX + (elapsed - holdEndTime) * TRACE_SPEED * 1.5;
}
// Camera/viewport
var headSX, viewOff;
var headSXEcg = headScreenRatio * vw;
if (headWX <= textStartWX) {
viewOff = Math.max(0, headWX - headSXEcg);
headSX = headWX - viewOff;
} else if (headWX >= textEndWX || isExitPhase) {
viewOff = textEndWX - finalHeadSX;
headSX = headWX - viewOff;
} else {
var p = (headWX - textStartWX) / (textEndWX - textStartWX);
headSX = headSXEcg + p * (finalHeadSX - headSXEcg);
viewOff = headWX - headSX;
}
var fadeAlpha = isExitPhase ? Math.max(0, 1 - (elapsed - holdEndTime) / EXIT_TIME) : 1;
if (!bgTransitioned && elapsed >= textEndTime - 0.3) {
bgTransitioned = true;
bootScreen.style.transition = 'background 1200ms ease-out';
bootScreen.style.background = '#FFFFFF';
}
ctx.save();
ctx.globalAlpha = fadeAlpha;
// Draw trace
var traceStart = Math.max(0, Math.floor(viewOff));
var traceEnd = Math.min(Math.ceil(isExitPhase ? textEndWX : headWX), Math.ceil(viewOff + vw));
if (traceEnd > traceStart) {
// Glow layer
ctx.beginPath();
ctx.strokeStyle = 'rgba(0, 255, 65, 0.25)';
ctx.lineWidth = 6;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.shadowColor = lineColor;
ctx.shadowBlur = 14;
for (var wx = traceStart; wx <= traceEnd; wx++) {
var sx = wx - viewOff;
var sy = getYAtX(wx);
if (wx === traceStart) ctx.moveTo(sx, sy); else ctx.lineTo(sx, sy);
}
ctx.stroke();
// Core line
ctx.beginPath();
ctx.strokeStyle = lineColor;
ctx.lineWidth = 2;
ctx.shadowBlur = 4;
for (var wx2 = traceStart; wx2 <= traceEnd; wx2++) {
var sx2 = wx2 - viewOff;
var sy2 = getYAtX(wx2);
if (wx2 === traceStart) ctx.moveTo(sx2, sy2); else ctx.lineTo(sx2, sy2);
}
ctx.stroke();
}
// Exit flatline after text
if (isExitPhase) {
var exitStartSX = textEndWX - viewOff;
var exitEndSX = headWX - viewOff;
ctx.beginPath();
ctx.strokeStyle = lineColor;
ctx.lineWidth = 2;
ctx.shadowBlur = 8;
ctx.moveTo(exitStartSX, baselineY);
ctx.lineTo(exitEndSX, baselineY);
ctx.stroke();
}
// Draw revealed text characters
ctx.shadowColor = lineColor;
ctx.shadowBlur = 8;
ctx.font = 'bold ' + fontSize + 'px Arial, Helvetica, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'alphabetic';
ctx.lineWidth = 1.5 * scale;
ctx.strokeStyle = lineColor;
ctx.fillStyle = 'transparent';
for (var k = 0; k < textLayout.length; k++) {
var item = textLayout[k];
var letterProgress = (headWX - item.startX) / (item.endX - item.startX);
if (letterProgress > 0.3) {
var alpha = Math.min(1, (letterProgress - 0.3) * 1.43);
ctx.globalAlpha = fadeAlpha * alpha;
var lsx = item.centerX - viewOff;
ctx.strokeText(item.char, lsx, baselineY);
}
}
// Head dot
ctx.globalAlpha = fadeAlpha;
ctx.shadowBlur = 0;
if (headSX >= -20 && headSX <= vw + 20) {
var headY = isExitPhase ? baselineY : getYAtX(headWX);
var grad = ctx.createRadialGradient(headSX, headY, 0, headSX, headY, 20 * scale);
grad.addColorStop(0, 'rgba(255,255,255,0.8)');
grad.addColorStop(0.3, 'rgba(0,255,65,0.6)');
grad.addColorStop(1, 'rgba(0,255,65,0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(headSX, headY, 20 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = lineColor;
ctx.beginPath();
ctx.arc(headSX, headY, 3, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
// Scanlines overlay
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
for (var sly = 0; sly < vh; sly += 4) {
ctx.fillRect(0, sly + 2, vw, 2);
}
// Vignette
var vig = ctx.createRadialGradient(vw / 2, vh / 2, vh * 0.3, vw / 2, vh / 2, vh * 0.85);
vig.addColorStop(0, 'rgba(0,0,0,0)');
vig.addColorStop(1, 'rgba(0,0,0,0.4)');
ctx.fillStyle = vig;
ctx.fillRect(0, 0, vw, vh);
requestAnimationFrame(animate);
} }
requestAnimationFrame(animate);
} }
/** /**
* Final cleanup: fade out all SVG lines and reveal the CV content. * Final cleanup: fade out canvas and reveal the CV content.
*/ */
function finishECGPhase(overlay, bootScreen) { function finishAnimation(overlay, bootScreen) {
// Fade out all SVG lines
overlay.style.transition = 'opacity 500ms ease'; overlay.style.transition = 'opacity 500ms ease';
overlay.style.opacity = '0'; overlay.style.opacity = '0';
setTimeout(function() { setTimeout(function() {
// Remove overlays from DOM
if (overlay.parentNode) overlay.parentNode.removeChild(overlay); if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
if (bootScreen.parentNode) bootScreen.parentNode.removeChild(bootScreen); if (bootScreen.parentNode) bootScreen.parentNode.removeChild(bootScreen);
// Reveal CV content
var cvContent = document.getElementById('cv-content'); var cvContent = document.getElementById('cv-content');
cvContent.classList.add('revealed'); cvContent.classList.add('revealed');
// Initialize nav tracking, smooth scroll, skill gauges, and scroll reveal after reveal
initNavTracking(); initNavTracking();
initSkillGauges(); initSkillGauges();
initScrollReveal(); initScrollReveal();
@@ -1865,7 +1863,7 @@
child.style.transitionDelay = (i * 60) + 'ms'; child.style.transitionDelay = (i * 60) + 'ms';
}); });
// Only animate once stop observing after reveal // Only animate once — stop observing after reveal
revealObserver.unobserve(section); revealObserver.unobserve(section);
} }
}); });
-60
View File
@@ -1,60 +0,0 @@
# Guardrails
## Standard Guardrails
### Single file constraint
- **When**: Building the HTML file
- **Rule**: Everything must be in ONE .html file. CSS in a `<style>` tag, JS in a `<script>` tag wrapped in IIFE with 'use strict'. No external files except Google Fonts CDN links.
- **Why**: These are concept designs meant to be opened directly in a browser. No build step, no server.
### Boot sequence consistency
- **When**: Implementing the boot/typing phase
- **Rule**: Boot text must match exactly: "CLINICAL TERMINAL v3.2.1", "Initialising pharmacist profile...", SYSTEM/USER/ROLE/LOCATION labels with values, loading modules line, three [OK] lines, "---", and final ready line. Use Fira Code font, green #00ff41 for [OK] and values, cyan #00e5ff for labels, dim green #3a6b45 for other text.
- **Why**: Boot sequence is the shared identity across all 7 concepts. Must be identical.
### CV content accuracy
- **When**: Adding CV content to the final design
- **Rule**: Use the expanded CV_v4.md content. Key roles in order: Interim Head (May-Nov 2025), Deputy Head (Jul 2024-Present), High-Cost Drugs & Interface Pharmacist (May 2022-Jul 2024), Pharmacy Manager Tesco (Nov 2017-May 2022), Duty Pharmacy Manager Tesco (Aug 2016-Nov 2017). Include Mary Seacole Programme in education. Include key achievements with specific numbers (£14.6M, 14,000 patients, £2.6M, 70%, 200hrs, £1M).
- **Why**: The CV data must be accurate and complete. Missing roles or wrong dates would be a critical error.
### Google Fonts loading
- **When**: Setting up the HTML head
- **Rule**: Use preconnect links to fonts.googleapis.com AND fonts.gstatic.com (with crossorigin), then the font CSS link. Load ALL fonts specified for this concept. Test that fonts actually render (check for FOUT).
- **Why**: Fonts are critical to each design's identity. Missing fonts break the visual concept.
### Transition timing
- **When**: Building the boot-to-design transition
- **Rule**: Boot phase should take ~4 seconds. Transition animation should take 2-4 seconds. Total time from page load to fully revealed design: no more than 8-9 seconds. User should never be waiting more than ~8s to see the CV.
- **Why**: Too long and users will leave. Too short and the effect is lost.
### No console errors
- **When**: Writing JavaScript
- **Rule**: No errors in the browser console. Handle edge cases: elements that might not exist yet, resize handlers with proper cleanup, animation frames that should stop when complete.
- **Why**: Console errors suggest broken functionality and are a quality check failure.
### Responsive breakpoints
- **When**: Adding responsive CSS
- **Rule**: Must work at 3 breakpoints: desktop (>768px), tablet (<=768px), mobile (<=480px). Navigation must be usable at all sizes. Content must not overflow horizontally. Touch targets must be reasonable size.
- **Why**: CVs are often viewed on mobile devices.
### Scroll animation observer
- **When**: Implementing scroll-triggered animations
- **Rule**: Use IntersectionObserver, NOT scroll event listeners. Set appropriate threshold (0.1-0.15) and rootMargin. Add .visible class to trigger CSS transitions. Animations should only play once (don't re-trigger on scroll up).
- **Why**: IntersectionObserver is more performant and reliable than scroll listeners.
### No external dependencies beyond fonts
- **When**: Tempted to add a library
- **Rule**: Do NOT import any JS libraries, CSS frameworks, icon fonts, or CDN resources other than Google Fonts. Everything must be vanilla HTML/CSS/JS.
- **Why**: Self-contained concept files. Adding dependencies defeats the purpose.
## Project-Specific Guardrails
### SVG viewBox and responsiveness
- **When**: Creating ECG line SVGs and skill circle SVGs
- **Rule**: Always set viewBox on SVGs. Use relative units where possible. The ECG flatline SVG should span the full viewport - use viewBox="0 0 [width] [height]" and update on resize, OR use CSS width:100%.
- **Why**: SVGs without viewBox won't scale properly across viewport sizes and the ECG effect will break.
### Skill circle calculation
- **When**: Building SVG circular progress gauges
- **Rule**: The circumference formula is 2 * Math.PI * radius. stroke-dasharray = circumference. stroke-dashoffset = circumference * (1 - level/100). The circle MUST have transform:rotate(-90deg) or use a transform on the group to start progress from 12 o'clock position.
- **Why**: Wrong math or missing rotation produces circles that fill from the wrong position or have incorrect percentages.
-130
View File
@@ -1,130 +0,0 @@
# Progress Log
## Codebase Patterns
- Single self-contained HTML file. All CSS in a single `<style>` tag. All JS in a single `<script>` tag wrapped in an IIFE with 'use strict'.
- No frameworks, no build step. Only external dependency: Google Fonts via <link> tags.
- Boot sequence is shared: pure black #000 bg, Fira Code font, blinking green cursor (#00ff41, step-end animation), boot text typed line by line (~220ms apart), [OK] lines bright green bold, SYSTEM/USER/ROLE/LOCATION labels cyan #00e5ff, values bright green, other lines dim green #3a6b45.
- CV data source: Use the expanded CV_v4.md content. Key roles: Interim Head (May-Nov 2025), Deputy Head (Jul 2024-Present), High-Cost Drugs Pharmacist (May 2022-Jul 2024), Pharmacy Manager Tesco (Nov 2017-May 2022), Duty Pharmacy Manager Tesco (Aug 2016-Nov 2017).
- Use IntersectionObserver for scroll animations (not scroll event listeners).
- For smooth scroll on nav clicks: preventDefault, calculate offset, window.scrollTo with behavior:'smooth'.
- Output HTML file name convention: 4-vitals-monitor.html.
- ECG PQRST waveform shape: flat -> small P bump -> flat -> sharp Q dip -> tall R spike -> S dip -> flat -> gentle T wave -> flat.
- Use SVG paths with stroke-dasharray/stroke-dashoffset for line drawing animations.
- SVG circle skill gauges: circumference = 2*PI*r. stroke-dashoffset = circumference * (1 - level/100). rotate(-90deg) to start from top.
- Google Fonts: Plus Jakarta Sans, Inter Tight.
- Floating pill nav: NOT full width. max-width 600px, margin: 12px auto, border-radius 100px.
- ECG overlay: #ecg-overlay div at z-index 1001. SVG created dynamically with viewBox matching viewport dimensions.
- drawHeartbeat(svg, svgNS, vw, vh, cy, rHeight, color, duration, onComplete) — reusable PQRST waveform renderer. rHeight controls R peak amplitude. color and duration are per-beat customisable.
- finishECGPhase() handles cleanup: fades ECG lines, transitions boot bg to white, removes overlays, reveals CV. Task 3 will replace the call to this with expanded branching logic.
- requestAnimationFrame needed before setting CSS transitions on dynamically created SVG elements.
- SVG Z (closepath) draws back to the M (moveTo) point, NOT to the last subpath start. Don't use Z when tracing from an external origin to a rectangle — use explicit L commands instead.
- startBranching(svg, svgNS, vw, vh, cy, overlay, bootScreen) — creates branch paths from peak. finishECGPhase(overlay, bootScreen) — fades SVG, removes overlays from DOM, reveals CV content.
- Color interpolation for 3 beats: #00ff41 (green) → #00C9A7 (midpoint) → #00897B (teal). Match glow filter color to stroke color.
- Scroll reveal CSS specificity trap: compound selectors like `.cv-main section.visible .card` that set `transform` will override `.card:hover { transform }` because (0,3,0) > (0,2,0). Must add matching-specificity hover rules: `.cv-main section.visible .card:hover { transform: ... }`.
- initScrollReveal() is called from finishECGPhase() alongside initNavTracking() and initSkillGauges().
## Iteration Log
### Iteration 1 — Task 1: Build the boot screen foundation
- Created `4-vitals-monitor.html` with full boot screen implementation
- Boot sequence follows guardrail format exactly: CLINICAL TERMINAL v3.2.1, profile init, SYSTEM/USER/ROLE/LOCATION, modules, [OK] lines, READY
- Timing: 14 lines × 220ms = ~2860ms boot + 400ms pause + 800ms fade = ~4.06s total (within guardrail)
- Boot text fades to opacity:0, then boot screen is removed and CV content revealed
- Temporary: boot screen hides directly after fade (Task 2 will insert ECG phase between fade and reveal)
- CSS variables, all 3 Google Font families loaded, IIFE strict mode JS
- Learnings: setTimeout delay argument is evaluated at call-time, not in the closure callback, so using a cumulative var in forEach works correctly for staggered timing
### Iteration 2 — Task 2: Build the ECG flatline and first heartbeat
- Added ECG overlay phase between boot fade and CV reveal
- Flatline draws left-to-right (1000ms linear) using SVG stroke-dasharray/dashoffset
- First PQRST heartbeat (R peak 40px) draws over 600ms at viewport center
- drawHeartbeat() is a reusable function accepting: svg, svgNS, vw, vh, cy, rHeight, color, duration, onComplete callback — Task 3 calls this with different amplitudes
- finishECGPhase() is a temporary cleanup function that Task 3 will replace with expanded logic (2nd/3rd beats + branching)
- Timing: boot ~4s + ECG ~2.4s = ~6.4s total (within 8-9s guardrail)
- Learnings: SVG paths created via createElementNS need the SVG namespace 'http://www.w3.org/2000/svg'. requestAnimationFrame is needed before setting CSS transitions on dynamically created SVG elements to ensure the initial state is painted first.
- The ECG overlay z-index is 1001 (above boot screen at 1000) so the lines render on top of the black background
### Iteration 3 — Task 3: Build second and third heartbeats with overflow branching
- Replaced finishECGPhase() call after 1st beat with chained sequence: 1st→2nd→3rd→branching→reveal
- Second heartbeat: R peak 60px, stroke #00C9A7 (green-teal midpoint), bg lightens to #0A0A0A
- Third heartbeat: R peak 100px, stroke #00897B (full teal), bg lightens to #141414
- startBranching() creates 7 SVG branch paths from the 3rd R peak apex:
- Branch 1: traces pill nav bar outline (rounded rect with Q curves for corners)
- Branches 2-3: trace hero section left/right vertical edges
- Branches 4-7: trace four vital sign card outlines (Q curve from peak to card top-left, then L lines around rectangle)
- Branches staggered by 50-150ms, 800ms draw with cubic-bezier easing
- Background transitions to white during branching (800ms ease-out)
- finishECGPhase() now accepts overlay/bootScreen params, removes elements from DOM (not just display:none)
- Glow filter on heartbeat paths dynamically matched to stroke color
- Total timing: ~8.5s (boot ~3.3s + flatline 1.05s + 3 beats ~2.55s + branching ~1.2s + fade 0.5s)
- Learnings: Don't use SVG Z (closepath) when tracing from an external origin to a rectangle — Z draws back to M point (the peak), not the rectangle's start corner. Instead, explicitly L back to the first rectangle corner.
- Learnings: When dynamically setting filter CSS on SVG elements, match the glow color to the stroke color for visual coherence.
### Iteration 4 — Task 4: Build final design skeleton with floating pill nav and typography
- Added floating pill nav bar: position: fixed; top: 16px; centered via left: 50% + translateX(-50%); max-width: 600px; border-radius: 999px; with subtle border and shadow
- Nav links use Inter Tight (--font-secondary) at 13px, muted color, with teal hover/active states
- Active nav link has a 4px teal dot below via ::after pseudo-element
- IntersectionObserver for active section tracking: threshold 0.3, rootMargin '-20% 0px -60% 0px'
- Smooth scroll: preventDefault on nav clicks, scrollTo with offset -70 and behavior: 'smooth'
- initNavTracking() called from finishECGPhase() after CV content is revealed (DOM must exist first)
- Main container: max-width 1000px, padding 0 32px, sections 80px vertical padding
- .section-heading utility class: 24px, 700 weight, heading color, margin-bottom 32px
- Responsive 768px: pill nav becomes full-width scrollable (overflow-x: auto, hidden scrollbar), main padding 20px
- Responsive 480px: nav links shrink to 11px/4px 8px padding, main padding 16px, section padding 48px
- All 6 sections added as skeleton: about, skills, experience, education, projects, contact + footer
- No new codebase pattern learnings — existing patterns from progress.txt covered all needs
### Iteration 5 — Task 5: Build hero section with vital sign cards
- Added centered hero section (#about) with min-height: 100vh, flexbox centering
- Content: h1 name (clamp 36-52px), muted job title, teal location pill, summary paragraph (max-width 560px)
- 4 vital sign metric cards in a flex row with gap 16px, teal border-top, hover elevation
- Card values use different font sizes: "10+" at 28px, "Python/SQL/BI" at 16px (.small), "Pop. Health" and "NHS N&W" at 18px (.medium)
- Responsive: 768px → 2x2 CSS grid, 480px → single column stacked + h1 reduced to 28px
- Vital cards have smooth hover transition: translateY(-2px) + shadow-md elevation
- No new codebase pattern learnings — straightforward implementation following established patterns
### Iteration 6 — Task 6: Build skills section with circular SVG progress gauges
- Skills HTML/CSS/JS was already substantially built in previous iterations (5 built the HTML structure)
- Polish additions: coral hover background for clinical skill items using `[data-color="coral"]:hover` selector
- Moved `text-align: center` from inline style to `.section-heading` class for cleanliness
- Confirmed responsive gauge sizing at 480px: CSS `width: 64px; height: 64px` overrides SVG presentation attributes since `viewBox` handles scaling
- 18 skills total: 8 Technical (teal), 6 Clinical (coral), 4 Strategic (teal) — all with correct percentages
- IntersectionObserver-based scroll animation with 100ms stagger is in place via `initSkillGauges()`
- Learnings: SVG `width`/`height` attributes are presentation attributes; CSS can override them when a `viewBox` is set
### Iteration 7 — Task 7: Build experience section with timeline and ECG decoration
- Added complete experience section with 5 roles in a vertical timeline layout
- Timeline has vertical line at 20% from left with dots (`.current` class fills dot with teal for active roles)
- Decorative ECG waveform SVG (200x30, PQRST shape) beside section heading at 30% opacity
- 5 timeline cards with role title, organisation (teal), date pill, and bullet points
- Cards have hover effect: scale(1.01), shadow-md elevation, teal left border appears
- Responsive 768px: timeline line and dots hidden, entries become full-width stacked
- Responsive 480px: reduced card padding (16px), smaller ECG decoration (120px via CSS width)
- All CV content verified accurate: correct titles, orgs, dates, key numbers (£14.6M, 14,000, £2.6M, 70%, 200+, £220M, £1M+, 3,000+)
- HTML tag balance verified: all section/div/ul/li/h3 tags matched correctly
- No new codebase pattern learnings — timeline pattern straightforward with existing conventions
### Iteration 8 — Task 8: Build education, projects, contact, and footer sections
- Added education section with 2-column grid: MPharm (Hons) UEA (2011-2015, 2:1) and Mary Seacole Leadership Programme (NHS Leadership Academy, 2018)
- Education cards use gradient top border via ::before pseudo-element (teal→coral) with overflow:hidden on card
- A-Levels line below cards: Mathematics (A*), Chemistry (B), Politics (C)
- Projects section with 2x2 grid: PharMetrics (with live link), Patient Pathway Analysis, Blueteq Generator, NMS Video
- Project cards use the gradient border hover trick: ::before with mask-composite exclude, opacity 0→1 on hover
- Contact section with 4-column grid using unicode icons: ☎ phone, ✉ email, ↗ LinkedIn, ○ location
- Footer with decorative ECG waveform SVG (120x20, PQRST shape) and attribution text
- Responsive 768px: projects grid → 1fr (single column), contact grid → 2x2
- Responsive 480px: education grid → 1fr, contact grid → 2x2
- All 13 content strings verified present, all HTML tags balanced (83 div pairs, 6 sections, 19 p, 9 a, 20 svg, etc.)
- CSS was already pre-built in previous iterations (education, projects, contact, footer classes all existed) — only HTML content needed to be filled in
- No new codebase pattern learnings — straightforward content population following established CSS patterns
### Iteration 9 — Task 9: Implement scroll animations and responsive design
- Added 3rd IntersectionObserver (`initScrollReveal()`) for section scroll-reveal animations
- All sections (except hero) start at `opacity: 0; transform: translateY(24px)` and animate to visible with `transition: opacity 0.6s ease, transform 0.6s ease`
- Hero section explicitly set to `opacity: 1; transform: none; transition: none` (always visible, above fold)
- Child elements (vital-card, skill-item, timeline-entry, education-card, project-card, contact-item) have staggered reveal: `opacity: 0; translateY(16px)` → visible state, with JS-assigned `transitionDelay` of `i * 60ms`
- Observer uses `threshold: 0.15` and `unobserve()` after reveal — animations only fire once
- Critical fix: hover transforms on cards need matching specificity to the `.cv-main section.visible .card` selectors, otherwise the `transform: translateY(0)` from the visible state overrides hover `transform: translateY(-2px)`. Added explicit hover rules at `.cv-main section.visible .education-card:hover` etc.
- Same specificity issue with `.cv-main .hero .vital-card` (0,3,0) overriding `.vital-card:hover` (0,2,0) — added `.cv-main .hero .vital-card:hover` at matching specificity
- Learnings: When using compound selectors for scroll-reveal states that set `transform`, ALWAYS add matching-specificity hover rules for any elements that also have hover transforms. The cascade means the higher-specificity non-hover rule will win over a lower-specificity hover rule.
- Responsive design was already largely complete from previous iterations (768px and 480px breakpoints). Task 9 primarily added the scroll animation layer on top.