1888 lines
61 KiB
HTML
1888 lines
61 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Andy Charlwood — MPharm | CV</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<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">
|
|
<style>
|
|
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
: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;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--font-primary);
|
|
font-size: 15px;
|
|
line-height: 1.7;
|
|
margin: 0;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
/* =========================================
|
|
BOOT SCREEN
|
|
========================================= */
|
|
|
|
#boot-screen {
|
|
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;
|
|
}
|
|
|
|
#boot-lines {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
max-width: 640px;
|
|
}
|
|
|
|
.boot-line {
|
|
opacity: 0;
|
|
transform: translateY(8px);
|
|
transition: opacity 400ms ease-out, transform 400ms ease-out;
|
|
white-space: nowrap;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.boot-line.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.boot-line.fading {
|
|
opacity: 0;
|
|
transition: opacity 800ms ease;
|
|
}
|
|
|
|
.c-green { color: #00ff41; }
|
|
.c-green-bold { color: #00ff41; font-weight: 700; }
|
|
.c-cyan { color: #00e5ff; }
|
|
.c-dim { color: #3a6b45; }
|
|
.c-grey { color: #666; }
|
|
|
|
/* Blinking cursor */
|
|
#boot-cursor {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 16px;
|
|
background: #00ff41;
|
|
animation: blink 1s step-end infinite;
|
|
vertical-align: middle;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0; }
|
|
}
|
|
|
|
/* =========================================
|
|
ECG OVERLAY
|
|
========================================= */
|
|
|
|
#ecg-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 1001;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#ecg-overlay canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
|
|
.ecg-seed-dot {
|
|
display: inline;
|
|
color: #00ff41;
|
|
}
|
|
|
|
.ecg-seed-dot.glowing {
|
|
text-shadow: 0 0 8px #00ff41, 0 0 16px #00ff41;
|
|
animation: seedPulse 0.6s ease-in-out infinite;
|
|
}
|
|
|
|
#travel-dot {
|
|
position: fixed;
|
|
z-index: 1002;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
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); }
|
|
}
|
|
|
|
/* =========================================
|
|
FINAL CV CONTENT (hidden behind boot)
|
|
========================================= */
|
|
|
|
#cv-content {
|
|
opacity: 0;
|
|
}
|
|
|
|
#cv-content.revealed {
|
|
opacity: 1;
|
|
transition: opacity 600ms ease;
|
|
}
|
|
|
|
/* =========================================
|
|
FLOATING PILL NAV
|
|
========================================= */
|
|
|
|
.pill-nav {
|
|
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;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.pill-nav a {
|
|
position: relative;
|
|
font-family: var(--font-secondary);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--muted);
|
|
text-decoration: none;
|
|
padding: 6px 14px;
|
|
border-radius: 999px;
|
|
transition: color 0.3s ease, background 0.3s ease;
|
|
}
|
|
|
|
.pill-nav a:hover {
|
|
color: var(--teal);
|
|
background: var(--teal-light);
|
|
}
|
|
|
|
.pill-nav a.active {
|
|
color: var(--teal);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pill-nav a.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 4px;
|
|
height: 4px;
|
|
border-radius: 50%;
|
|
background: var(--teal);
|
|
}
|
|
|
|
/* =========================================
|
|
MAIN CONTAINER & SECTIONS
|
|
========================================= */
|
|
|
|
.cv-main {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
padding: 0 32px;
|
|
}
|
|
|
|
.cv-main section {
|
|
padding: 80px 0;
|
|
}
|
|
|
|
.section-heading {
|
|
font-family: var(--font-primary);
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: var(--heading);
|
|
margin-bottom: 32px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* =========================================
|
|
HERO SECTION
|
|
========================================= */
|
|
|
|
.hero {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
text-align: center;
|
|
}
|
|
|
|
.hero h1 {
|
|
font-family: var(--font-primary);
|
|
font-weight: 700;
|
|
font-size: clamp(36px, 5vw, 52px);
|
|
color: var(--heading);
|
|
line-height: 1.2;
|
|
margin: 0;
|
|
}
|
|
|
|
.hero-title {
|
|
font-size: 16px;
|
|
color: var(--muted);
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.hero-location {
|
|
display: inline-block;
|
|
padding: 4px 16px;
|
|
border: 1px solid var(--teal);
|
|
border-radius: 999px;
|
|
font-size: 12px;
|
|
color: var(--teal);
|
|
font-weight: 500;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.hero-summary {
|
|
font-size: 15px;
|
|
line-height: 1.8;
|
|
max-width: 560px;
|
|
color: var(--text);
|
|
margin: 24px auto 0;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Vital sign metric cards */
|
|
.vitals-row {
|
|
display: flex;
|
|
gap: 16px;
|
|
margin-top: 40px;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.vital-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;
|
|
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.vital-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.vital-value {
|
|
font-family: var(--font-primary);
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: var(--heading);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.vital-value.small {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.vital-value.medium {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.vital-label {
|
|
font-family: var(--font-secondary);
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--muted);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* =========================================
|
|
SKILLS SECTION
|
|
========================================= */
|
|
|
|
.skills-category {
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.skills-category:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.skills-cat-label {
|
|
font-family: var(--font-secondary);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--muted);
|
|
margin-bottom: 20px;
|
|
padding-left: 4px;
|
|
}
|
|
|
|
.skills-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
gap: 24px;
|
|
}
|
|
|
|
.skill-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 16px;
|
|
border-radius: var(--radius);
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.skill-item:hover {
|
|
background: var(--teal-light);
|
|
}
|
|
|
|
.skill-item[data-color="coral"]:hover {
|
|
background: var(--coral-light);
|
|
}
|
|
|
|
.skill-gauge {
|
|
display: block;
|
|
}
|
|
|
|
.skill-progress {
|
|
stroke-dasharray: 213.628;
|
|
stroke-dashoffset: 213.628;
|
|
transition: stroke-dashoffset 1.2s ease-out;
|
|
}
|
|
|
|
.skill-name {
|
|
font-family: var(--font-primary);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--heading);
|
|
margin-top: 8px;
|
|
text-align: center;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.skill-cat {
|
|
font-family: var(--font-secondary);
|
|
font-size: 10px;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* =========================================
|
|
EXPERIENCE SECTION
|
|
========================================= */
|
|
|
|
.experience-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.experience-header h2 {
|
|
font-family: var(--font-primary);
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: var(--heading);
|
|
margin: 0;
|
|
}
|
|
|
|
.ecg-decoration {
|
|
display: block;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.timeline {
|
|
position: relative;
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 20%;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: var(--teal);
|
|
opacity: 0.2;
|
|
}
|
|
|
|
.timeline-entry {
|
|
position: relative;
|
|
padding-left: calc(20% + 32px);
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.timeline-entry:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.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);
|
|
z-index: 1;
|
|
}
|
|
|
|
.timeline-dot.current {
|
|
background: var(--teal);
|
|
}
|
|
|
|
.timeline-card {
|
|
background: var(--card-bg);
|
|
border-radius: var(--radius);
|
|
padding: 24px;
|
|
box-shadow: var(--shadow-sm);
|
|
border-left: 3px solid transparent;
|
|
transition: box-shadow 0.3s ease, transform 0.3s ease, border-left-color 0.3s ease;
|
|
}
|
|
|
|
.timeline-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
transform: scale(1.01);
|
|
border-left-color: rgba(0, 137, 123, 0.3);
|
|
}
|
|
|
|
.timeline-role {
|
|
font-family: var(--font-primary);
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
color: var(--heading);
|
|
margin: 0;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.timeline-org {
|
|
font-family: var(--font-primary);
|
|
font-size: 14px;
|
|
color: var(--teal);
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.timeline-date {
|
|
display: inline-block;
|
|
padding: 2px 10px;
|
|
background: var(--teal-light);
|
|
border-radius: 999px;
|
|
font-family: var(--font-secondary);
|
|
font-size: 12px;
|
|
color: var(--teal);
|
|
font-weight: 500;
|
|
margin: 6px 0 12px;
|
|
}
|
|
|
|
.timeline-bullets {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.timeline-bullets li {
|
|
font-size: 14px;
|
|
line-height: 1.7;
|
|
margin: 4px 0;
|
|
padding-left: 15px;
|
|
position: relative;
|
|
color: var(--text);
|
|
}
|
|
|
|
.timeline-bullets li::before {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 5px;
|
|
height: 5px;
|
|
border-radius: 50%;
|
|
background: var(--teal);
|
|
position: absolute;
|
|
left: 0;
|
|
top: 10px;
|
|
}
|
|
|
|
/* =========================================
|
|
EDUCATION SECTION
|
|
========================================= */
|
|
|
|
.education-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 20px;
|
|
}
|
|
|
|
.education-card {
|
|
position: relative;
|
|
background: var(--card-bg);
|
|
border-radius: var(--radius);
|
|
padding: 24px;
|
|
box-shadow: var(--shadow-sm);
|
|
overflow: hidden;
|
|
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.education-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(to right, var(--teal), var(--coral));
|
|
}
|
|
|
|
.education-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.education-degree {
|
|
font-family: var(--font-primary);
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
color: var(--heading);
|
|
margin: 0;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.education-institution {
|
|
font-size: 14px;
|
|
color: var(--teal);
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.education-period {
|
|
font-size: 13px;
|
|
color: var(--muted);
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.education-detail {
|
|
font-size: 14px;
|
|
color: var(--text);
|
|
margin: 6px 0 0;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.education-alevels {
|
|
font-size: 13px;
|
|
color: var(--muted);
|
|
margin-top: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* =========================================
|
|
PROJECTS SECTION
|
|
========================================= */
|
|
|
|
.projects-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 20px;
|
|
}
|
|
|
|
.project-card {
|
|
position: relative;
|
|
background: var(--card-bg);
|
|
border-radius: var(--radius);
|
|
padding: 24px;
|
|
box-shadow: var(--shadow-sm);
|
|
overflow: hidden;
|
|
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.project-card::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 ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.project-card:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.project-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.project-title {
|
|
font-family: var(--font-primary);
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--heading);
|
|
margin: 0;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.project-desc {
|
|
font-size: 14px;
|
|
color: var(--text);
|
|
line-height: 1.7;
|
|
margin: 8px 0 0;
|
|
}
|
|
|
|
.project-link {
|
|
display: inline-block;
|
|
padding: 6px 16px;
|
|
background: var(--teal);
|
|
color: #fff;
|
|
border-radius: 999px;
|
|
font-family: var(--font-secondary);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
margin-top: 12px;
|
|
transition: background 0.3s ease, transform 0.2s ease;
|
|
}
|
|
|
|
.project-link:hover {
|
|
background: #00796B;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* =========================================
|
|
CONTACT SECTION
|
|
========================================= */
|
|
|
|
.contact-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 16px;
|
|
}
|
|
|
|
.contact-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.contact-icon {
|
|
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;
|
|
line-height: 1;
|
|
}
|
|
|
|
.contact-value {
|
|
font-family: var(--font-secondary);
|
|
font-size: 13px;
|
|
color: var(--heading);
|
|
word-break: break-word;
|
|
}
|
|
|
|
.contact-value a {
|
|
color: var(--teal);
|
|
text-decoration: none;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.contact-value a:hover {
|
|
color: #00796B;
|
|
}
|
|
|
|
.contact-label {
|
|
font-family: var(--font-secondary);
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* =========================================
|
|
SCROLL REVEAL ANIMATIONS
|
|
========================================= */
|
|
|
|
.cv-main section {
|
|
opacity: 0;
|
|
transform: translateY(24px);
|
|
transition: opacity 0.6s ease, transform 0.6s ease;
|
|
}
|
|
|
|
.cv-main section.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
/* Hero section should be immediately visible (above fold) */
|
|
.cv-main section.hero {
|
|
opacity: 1;
|
|
transform: none;
|
|
transition: none;
|
|
}
|
|
|
|
/* Staggered child card/item animations */
|
|
.cv-main section .vital-card,
|
|
.cv-main section .skill-item,
|
|
.cv-main section .timeline-entry,
|
|
.cv-main section .education-card,
|
|
.cv-main section .project-card,
|
|
.cv-main section .contact-item {
|
|
opacity: 0;
|
|
transform: translateY(16px);
|
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
}
|
|
|
|
.cv-main section.visible .vital-card,
|
|
.cv-main section.visible .skill-item,
|
|
.cv-main section.visible .timeline-entry,
|
|
.cv-main section.visible .education-card,
|
|
.cv-main section.visible .project-card,
|
|
.cv-main section.visible .contact-item {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
/* Re-enable hover transforms at matching specificity */
|
|
.cv-main section.visible .education-card:hover,
|
|
.cv-main section.visible .project-card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.cv-main section.visible .timeline-entry .timeline-card:hover {
|
|
transform: scale(1.01);
|
|
}
|
|
|
|
/* Hero vital cards get their own reveal since hero is always visible */
|
|
.cv-main .hero .vital-card {
|
|
opacity: 1;
|
|
transform: none;
|
|
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.cv-main .hero .vital-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* =========================================
|
|
FOOTER
|
|
========================================= */
|
|
|
|
.cv-footer {
|
|
text-align: center;
|
|
padding: 48px 0 32px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.footer-ecg {
|
|
display: block;
|
|
margin: 0 auto 12px;
|
|
}
|
|
|
|
.footer-text {
|
|
font-family: var(--font-secondary);
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* =========================================
|
|
RESPONSIVE: 768px
|
|
========================================= */
|
|
|
|
@media (max-width: 768px) {
|
|
.pill-nav {
|
|
max-width: 100%;
|
|
width: calc(100% - 32px);
|
|
overflow-x: auto;
|
|
padding: 6px 12px;
|
|
-webkit-overflow-scrolling: touch;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
.pill-nav::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.cv-main {
|
|
padding: 0 20px;
|
|
}
|
|
|
|
.vitals-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.skills-grid {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
|
|
.timeline::before {
|
|
display: none;
|
|
}
|
|
|
|
.timeline-dot {
|
|
display: none;
|
|
}
|
|
|
|
.timeline-entry {
|
|
padding-left: 0;
|
|
}
|
|
|
|
.projects-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.contact-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
|
|
/* =========================================
|
|
RESPONSIVE: 480px
|
|
========================================= */
|
|
|
|
@media (max-width: 480px) {
|
|
.pill-nav a {
|
|
font-size: 11px;
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.cv-main {
|
|
padding: 0 16px;
|
|
}
|
|
|
|
.cv-main section {
|
|
padding: 48px 0;
|
|
}
|
|
|
|
.hero h1 {
|
|
font-size: 28px;
|
|
}
|
|
|
|
.vitals-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.skills-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 16px;
|
|
}
|
|
|
|
.skill-gauge {
|
|
width: 64px;
|
|
height: 64px;
|
|
}
|
|
|
|
.skill-item {
|
|
padding: 12px;
|
|
}
|
|
|
|
.timeline-card {
|
|
padding: 16px;
|
|
}
|
|
|
|
.experience-header {
|
|
gap: 10px;
|
|
}
|
|
|
|
.ecg-decoration {
|
|
width: 120px;
|
|
}
|
|
|
|
.education-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.contact-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Boot Screen Overlay -->
|
|
<div id="boot-screen">
|
|
<div id="boot-lines"></div>
|
|
</div>
|
|
|
|
<!-- ECG Overlay (created dynamically but container lives here) -->
|
|
<div id="ecg-overlay"></div>
|
|
|
|
<!-- Final CV Content (hidden behind boot screen until transition) -->
|
|
<div id="cv-content">
|
|
|
|
<!-- Floating Pill Navigation -->
|
|
<nav class="pill-nav" id="pill-nav">
|
|
<a href="#about" data-section="about">About</a>
|
|
<a href="#skills" data-section="skills">Skills</a>
|
|
<a href="#experience" data-section="experience">Experience</a>
|
|
<a href="#education" data-section="education">Education</a>
|
|
<a href="#projects" data-section="projects">Projects</a>
|
|
<a href="#contact" data-section="contact">Contact</a>
|
|
</nav>
|
|
|
|
<!-- Main CV Content -->
|
|
<main class="cv-main">
|
|
|
|
<section id="about" class="hero">
|
|
<h1>Andy Charlwood</h1>
|
|
<p class="hero-title">Deputy Head of Population Health & Data Analysis</p>
|
|
<span class="hero-location">Norwich, UK</span>
|
|
<p class="hero-summary">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.</p>
|
|
|
|
<div class="vitals-row">
|
|
<div class="vital-card">
|
|
<div class="vital-value">10+</div>
|
|
<div class="vital-label">Years Experience</div>
|
|
</div>
|
|
<div class="vital-card">
|
|
<div class="vital-value small">Python/SQL/BI</div>
|
|
<div class="vital-label">Analytics Stack</div>
|
|
</div>
|
|
<div class="vital-card">
|
|
<div class="vital-value medium">Pop. Health</div>
|
|
<div class="vital-label">Focus Area</div>
|
|
</div>
|
|
<div class="vital-card">
|
|
<div class="vital-value medium">NHS N&W</div>
|
|
<div class="vital-label">System</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="skills">
|
|
<h2 class="section-heading">Skills & Expertise</h2>
|
|
|
|
<div class="skills-category">
|
|
<h3 class="skills-cat-label">Technical</h3>
|
|
<div class="skills-grid">
|
|
<div class="skill-item" data-level="90" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">90%</text>
|
|
</svg>
|
|
<span class="skill-name">Python</span>
|
|
<span class="skill-cat">Technical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="88" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">88%</text>
|
|
</svg>
|
|
<span class="skill-name">SQL</span>
|
|
<span class="skill-cat">Technical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="92" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">92%</text>
|
|
</svg>
|
|
<span class="skill-name">Power BI</span>
|
|
<span class="skill-cat">Technical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="70" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">70%</text>
|
|
</svg>
|
|
<span class="skill-name">JS / TS</span>
|
|
<span class="skill-cat">Technical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="95" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">95%</text>
|
|
</svg>
|
|
<span class="skill-name">Data Analysis</span>
|
|
<span class="skill-cat">Technical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="88" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">88%</text>
|
|
</svg>
|
|
<span class="skill-name">Dashboard Dev</span>
|
|
<span class="skill-cat">Technical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="82" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">82%</text>
|
|
</svg>
|
|
<span class="skill-name">Algorithm Design</span>
|
|
<span class="skill-cat">Technical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="80" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">80%</text>
|
|
</svg>
|
|
<span class="skill-name">Data Pipelines</span>
|
|
<span class="skill-cat">Technical</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="skills-category">
|
|
<h3 class="skills-cat-label">Clinical</h3>
|
|
<div class="skills-grid">
|
|
<div class="skill-item" data-level="95" data-color="coral">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--coral)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">95%</text>
|
|
</svg>
|
|
<span class="skill-name">Medicines Optimisation</span>
|
|
<span class="skill-cat">Clinical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="90" data-color="coral">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--coral)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">90%</text>
|
|
</svg>
|
|
<span class="skill-name">Pop. Health Analytics</span>
|
|
<span class="skill-cat">Clinical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="85" data-color="coral">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--coral)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">85%</text>
|
|
</svg>
|
|
<span class="skill-name">NICE TA</span>
|
|
<span class="skill-cat">Clinical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="80" data-color="coral">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--coral)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">80%</text>
|
|
</svg>
|
|
<span class="skill-name">Health Economics</span>
|
|
<span class="skill-cat">Clinical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="82" data-color="coral">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--coral)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">82%</text>
|
|
</svg>
|
|
<span class="skill-name">Clinical Pathways</span>
|
|
<span class="skill-cat">Clinical</span>
|
|
</div>
|
|
<div class="skill-item" data-level="88" data-color="coral">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--coral)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">88%</text>
|
|
</svg>
|
|
<span class="skill-name">CD Assurance</span>
|
|
<span class="skill-cat">Clinical</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="skills-category">
|
|
<h3 class="skills-cat-label">Strategic</h3>
|
|
<div class="skills-grid">
|
|
<div class="skill-item" data-level="90" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">90%</text>
|
|
</svg>
|
|
<span class="skill-name">Budget Mgmt</span>
|
|
<span class="skill-cat">Strategic</span>
|
|
</div>
|
|
<div class="skill-item" data-level="88" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">88%</text>
|
|
</svg>
|
|
<span class="skill-name">Stakeholder Engagement</span>
|
|
<span class="skill-cat">Strategic</span>
|
|
</div>
|
|
<div class="skill-item" data-level="85" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">85%</text>
|
|
</svg>
|
|
<span class="skill-name">Pharma Negotiation</span>
|
|
<span class="skill-cat">Strategic</span>
|
|
</div>
|
|
<div class="skill-item" data-level="82" data-color="teal">
|
|
<svg class="skill-gauge" width="80" height="80" viewBox="0 0 80 80">
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="var(--border)" stroke-width="5"/>
|
|
<circle class="skill-progress" cx="40" cy="40" r="34" fill="none" stroke="var(--teal)" stroke-width="5" stroke-linecap="round" transform="rotate(-90, 40, 40)"/>
|
|
<text x="40" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="600" fill="var(--heading)" font-family="var(--font-secondary)">82%</text>
|
|
</svg>
|
|
<span class="skill-name">Team Development</span>
|
|
<span class="skill-cat">Strategic</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="experience">
|
|
<div class="experience-header">
|
|
<h2>Experience</h2>
|
|
<svg class="ecg-decoration" width="200" height="30" viewBox="0 0 200 30" fill="none">
|
|
<path d="M 0 15 L 40 15 L 50 15 C 53 15 55 12 58 12 C 61 12 63 15 66 15 L 76 15 L 80 20 L 86 2 L 92 22 L 96 15 L 106 15 C 109 15 111 11 114 11 C 117 11 120 15 123 15 L 200 15" stroke="var(--teal)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="timeline">
|
|
|
|
<div class="timeline-entry">
|
|
<div class="timeline-dot current"></div>
|
|
<div class="timeline-card">
|
|
<h3 class="timeline-role">Interim Head of Population Health & Data Analysis</h3>
|
|
<p class="timeline-org">NHS Norfolk & Waveney ICB</p>
|
|
<span class="timeline-date">May 2025 — Nov 2025</span>
|
|
<ul class="timeline-bullets">
|
|
<li>Led team through organisational transition, maintaining delivery of £14.6M efficiency programme</li>
|
|
<li>Directed strategic priorities for population health analytics across Norfolk & Waveney (population ~1M)</li>
|
|
<li>Managed stakeholder relationships with system leaders, provider trusts, and primary care networks</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="timeline-entry">
|
|
<div class="timeline-dot current"></div>
|
|
<div class="timeline-card">
|
|
<h3 class="timeline-role">Deputy Head of Population Health & Data Analysis</h3>
|
|
<p class="timeline-org">NHS Norfolk & Waveney ICB</p>
|
|
<span class="timeline-date">Jul 2024 — Present</span>
|
|
<ul class="timeline-bullets">
|
|
<li>Deputised for Head of department across all operational and strategic functions</li>
|
|
<li>Oversaw £220M medicines budget and led programme of cost improvement initiatives</li>
|
|
<li>Developed Python-based switching algorithm processing 14,000 patients, delivering £2.6M savings</li>
|
|
<li>Built Blueteq automation system reducing processing time by 70%, saving 200+ hours annually</li>
|
|
<li>Created PharMetrics dashboard platform for real-time medicines expenditure tracking</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="timeline-entry">
|
|
<div class="timeline-dot"></div>
|
|
<div class="timeline-card">
|
|
<h3 class="timeline-role">High-Cost Drugs & Interface Pharmacist</h3>
|
|
<p class="timeline-org">NHS Norfolk & Waveney ICB</p>
|
|
<span class="timeline-date">May 2022 — Jul 2024</span>
|
|
<ul class="timeline-bullets">
|
|
<li>Managed high-cost drugs budget across acute and community settings</li>
|
|
<li>Led NICE Technology Appraisal implementation and horizon scanning</li>
|
|
<li>Developed health economic models for biosimilar switching programmes</li>
|
|
<li>Built data pipelines for automated reporting of medicines expenditure</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="timeline-entry">
|
|
<div class="timeline-dot"></div>
|
|
<div class="timeline-card">
|
|
<h3 class="timeline-role">Pharmacy Manager</h3>
|
|
<p class="timeline-org">Tesco Pharmacy</p>
|
|
<span class="timeline-date">Nov 2017 — May 2022</span>
|
|
<ul class="timeline-bullets">
|
|
<li>Managed community pharmacy delivering 3,000+ items monthly</li>
|
|
<li>Pioneered asthma screening service generating £1M+ national revenue</li>
|
|
<li>Led team of 6 through COVID-19 pandemic service delivery</li>
|
|
<li>Completed Mary Seacole NHS Leadership Programme (2018)</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="timeline-entry">
|
|
<div class="timeline-dot"></div>
|
|
<div class="timeline-card">
|
|
<h3 class="timeline-role">Duty Pharmacy Manager</h3>
|
|
<p class="timeline-org">Tesco Pharmacy</p>
|
|
<span class="timeline-date">Aug 2016 — Nov 2017</span>
|
|
<ul class="timeline-bullets">
|
|
<li>Supported pharmacy manager in daily operations and clinical services</li>
|
|
<li>Delivered Medicines Use Reviews and New Medicine Service consultations</li>
|
|
<li>Maintained controlled drug compliance and clinical governance standards</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<section id="education">
|
|
<h2 class="section-heading">Education</h2>
|
|
<div class="education-grid">
|
|
<div class="education-card">
|
|
<h3 class="education-degree">MPharm (Hons) Pharmacy</h3>
|
|
<p class="education-institution">University of East Anglia</p>
|
|
<p class="education-period">2011 — 2015</p>
|
|
<p class="education-detail">Upper Second-Class Honours (2:1)</p>
|
|
</div>
|
|
<div class="education-card">
|
|
<h3 class="education-degree">Mary Seacole Leadership Programme</h3>
|
|
<p class="education-institution">NHS Leadership Academy</p>
|
|
<p class="education-period">2018</p>
|
|
<p class="education-detail">National healthcare leadership development programme.</p>
|
|
</div>
|
|
</div>
|
|
<p class="education-alevels">A-Levels: Mathematics (A*), Chemistry (B), Politics (C)</p>
|
|
</section>
|
|
|
|
<section id="projects">
|
|
<h2 class="section-heading">Projects</h2>
|
|
<div class="projects-grid">
|
|
<div class="project-card">
|
|
<h3 class="project-title">PharMetrics</h3>
|
|
<p class="project-desc">Real-time medicines expenditure dashboard providing actionable analytics for NHS decision-makers.</p>
|
|
<a href="https://medicines.charlwood.xyz/" class="project-link" target="_blank" rel="noopener noreferrer">Visit Project</a>
|
|
</div>
|
|
<div class="project-card">
|
|
<h3 class="project-title">Patient Pathway Analysis</h3>
|
|
<p class="project-desc">Data-driven analysis of patient pathways to identify optimisation opportunities and improve clinical outcomes.</p>
|
|
</div>
|
|
<div class="project-card">
|
|
<h3 class="project-title">Blueteq Generator</h3>
|
|
<p class="project-desc">Automation tool reducing high-cost drug approval processing time by 70%, saving 200+ hours annually.</p>
|
|
</div>
|
|
<div class="project-card">
|
|
<h3 class="project-title">NMS Video</h3>
|
|
<p class="project-desc">Educational video resource supporting New Medicine Service consultations, improving patient engagement.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="contact">
|
|
<h2 class="section-heading">Contact</h2>
|
|
<div class="contact-grid">
|
|
<div class="contact-item">
|
|
<div class="contact-icon">☎</div>
|
|
<div class="contact-value">07795553088</div>
|
|
<div class="contact-label">Phone</div>
|
|
</div>
|
|
<div class="contact-item">
|
|
<div class="contact-icon">✉</div>
|
|
<div class="contact-value"><a href="mailto:andy@charlwood.xyz">andy@charlwood.xyz</a></div>
|
|
<div class="contact-label">Email</div>
|
|
</div>
|
|
<div class="contact-item">
|
|
<div class="contact-icon">↗</div>
|
|
<div class="contact-value"><a href="https://linkedin.com/in/andrewcharlwood" target="_blank" rel="noopener noreferrer">linkedin.com/in/andrewcharlwood</a></div>
|
|
<div class="contact-label">LinkedIn</div>
|
|
</div>
|
|
<div class="contact-item">
|
|
<div class="contact-icon">○</div>
|
|
<div class="contact-value">Norwich, UK</div>
|
|
<div class="contact-label">Location</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<footer class="cv-footer">
|
|
<svg class="footer-ecg" width="120" height="20" viewBox="0 0 120 20" fill="none">
|
|
<path d="M 0 10 L 35 10 L 40 10 C 42 10 43 7 45 7 C 47 7 48 10 50 10 L 54 10 L 56 13 L 60 2 L 64 15 L 66 10 L 70 10 C 72 10 73 7 75 7 C 77 7 78 10 80 10 L 120 10" stroke="var(--teal)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3" fill="none"/>
|
|
</svg>
|
|
<p class="footer-text">Andy Charlwood — MPharm, GPhC Registered Pharmacist</p>
|
|
</footer>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
/* =========================================
|
|
BOOT SEQUENCE
|
|
========================================= */
|
|
|
|
var bootLines = [
|
|
{ html: '<span class="c-green-bold">CLINICAL TERMINAL v3.2.1</span>', delay: 0 },
|
|
{ html: '<span class="c-dim">Initialising pharmacist profile...</span>', delay: 220 },
|
|
{ html: '<span class="c-dim">---</span>', delay: 220 },
|
|
{ html: '<span class="c-cyan">SYSTEM </span><span class="c-green">NHS Norfolk & Waveney ICB</span>', delay: 220 },
|
|
{ html: '<span class="c-cyan">USER </span><span class="c-green">Andy Charlwood</span>', delay: 220 },
|
|
{ html: '<span class="c-cyan">ROLE </span><span class="c-green">Deputy Head of Population Health & Data Analysis</span>', delay: 220 },
|
|
{ html: '<span class="c-cyan">LOCATION </span><span class="c-green">Norwich, UK</span>', delay: 220 },
|
|
{ html: '<span class="c-dim">---</span>', delay: 220 },
|
|
{ html: '<span class="c-dim">Loading modules...</span>', delay: 220 },
|
|
{ html: '<span class="c-green-bold">[OK]</span> <span class="c-dim">pharmacist_core.sys</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-dim">---</span>', delay: 220 },
|
|
{ html: '<span class="c-green-bold">> 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 totalBootTime = 0;
|
|
|
|
bootLines.forEach(function(line, index) {
|
|
totalBootTime += line.delay;
|
|
var el = document.createElement('div');
|
|
el.className = 'boot-line';
|
|
el.innerHTML = line.html;
|
|
bootContainer.appendChild(el);
|
|
|
|
setTimeout(function() {
|
|
el.classList.add('visible');
|
|
}, totalBootTime);
|
|
});
|
|
|
|
// After all lines visible, activate seed dot glow
|
|
var bootEndTime = totalBootTime + 400;
|
|
|
|
setTimeout(function() {
|
|
var seedDot = document.getElementById('ecg-seed-dot');
|
|
if (seedDot) seedDot.classList.add('glowing');
|
|
}, totalBootTime);
|
|
|
|
setTimeout(function() {
|
|
var cursor = document.getElementById('boot-cursor');
|
|
if (cursor) 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
|
|
var allLines = bootContainer.querySelectorAll('.boot-line');
|
|
for (var i = 0; i < allLines.length; i++) {
|
|
allLines[i].classList.add('fading');
|
|
}
|
|
}, bootEndTime);
|
|
|
|
// After fade + dot travel, start ECG name animation
|
|
setTimeout(function() {
|
|
var td = document.getElementById('travel-dot');
|
|
if (td) td.remove();
|
|
startECGNameAnimation();
|
|
}, bootEndTime + 800);
|
|
|
|
/* =========================================
|
|
ECG NAME ANIMATION (ported from Remotion)
|
|
========================================= */
|
|
|
|
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 bootScreen = document.getElementById('boot-screen');
|
|
var vw = window.innerWidth;
|
|
var vh = window.innerHeight;
|
|
var dpr = window.devicePixelRatio || 1;
|
|
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = vw * dpr;
|
|
canvas.height = vh * dpr;
|
|
overlay.appendChild(canvas);
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.scale(dpr, dpr);
|
|
|
|
// Responsive scale
|
|
var scale = Math.min(1.2, Math.max(0.35, vw / 1400));
|
|
var LETTER_W = 72 * scale;
|
|
var LETTER_G = 10 * scale;
|
|
var SPACE_W = 30 * scale;
|
|
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';
|
|
|
|
// Beats (time-based, widths scaled)
|
|
var beats = [
|
|
{ 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; });
|
|
|
|
var lastBeat = beats[beats.length - 1];
|
|
var lastBeatEndWX = lastBeat.startWX + lastBeat.widthPx;
|
|
var textStartWX = lastBeatEndWX + FLAT_GAP * TRACE_SPEED;
|
|
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);
|
|
|
|
var headScreenRatio = 0.75;
|
|
var finalHeadSX = (vw - totalTextW) / 2 + totalTextW;
|
|
var textEndTime = textEndWX / TRACE_SPEED;
|
|
var holdEndTime = textEndTime + HOLD_TIME;
|
|
var exitEndTime = holdEndTime + EXIT_TIME;
|
|
|
|
function getYAtX(wx) {
|
|
for (var i = 0; i < beats.length; i++) {
|
|
var b = beats[i];
|
|
if (wx >= b.startWX && wx <= b.startWX + b.widthPx) {
|
|
var prog = (wx - b.startWX) / b.widthPx;
|
|
var pts = generateHeartbeatPoints(b.amplitude);
|
|
var idx = Math.min(Math.floor(prog * (pts.length - 1)), pts.length - 1);
|
|
return baselineY - pts[idx].y * ecgMaxDefl;
|
|
}
|
|
}
|
|
for (var j = 0; j < textLayout.length; j++) {
|
|
var item = textLayout[j];
|
|
if (wx >= item.startX && wx <= item.endX) {
|
|
var t = (wx - item.startX) / (item.endX - item.startX);
|
|
var ld = ECG_LETTERS[item.char];
|
|
if (ld) return baselineY - interpolateLetterY(ld, t) * textMaxDefl;
|
|
}
|
|
}
|
|
return baselineY;
|
|
}
|
|
|
|
var startTs = null;
|
|
var bgTransitioned = false;
|
|
|
|
function animate(timestamp) {
|
|
if (!startTs) startTs = timestamp;
|
|
var elapsed = (timestamp - startTs) / 1000;
|
|
|
|
if (elapsed >= exitEndTime) {
|
|
finishAnimation(overlay, bootScreen);
|
|
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 canvas and reveal the CV content.
|
|
*/
|
|
function finishAnimation(overlay, bootScreen) {
|
|
overlay.style.transition = 'opacity 500ms ease';
|
|
overlay.style.opacity = '0';
|
|
|
|
setTimeout(function() {
|
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
if (bootScreen.parentNode) bootScreen.parentNode.removeChild(bootScreen);
|
|
|
|
var cvContent = document.getElementById('cv-content');
|
|
cvContent.classList.add('revealed');
|
|
|
|
initNavTracking();
|
|
initSkillGauges();
|
|
initScrollReveal();
|
|
}, 500);
|
|
}
|
|
|
|
/* =========================================
|
|
NAV: Active Section Tracking & Smooth Scroll
|
|
========================================= */
|
|
|
|
function initNavTracking() {
|
|
var navLinks = document.querySelectorAll('.pill-nav a');
|
|
var sections = document.querySelectorAll('.cv-main section');
|
|
|
|
// Smooth scroll on nav click
|
|
navLinks.forEach(function(link) {
|
|
link.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
var targetId = link.getAttribute('data-section');
|
|
var target = document.getElementById(targetId);
|
|
if (target) {
|
|
window.scrollTo({
|
|
top: target.offsetTop - 70,
|
|
behavior: 'smooth'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// IntersectionObserver for active section tracking
|
|
var observer = new IntersectionObserver(function(entries) {
|
|
entries.forEach(function(entry) {
|
|
if (entry.isIntersecting) {
|
|
var id = entry.target.getAttribute('id');
|
|
navLinks.forEach(function(link) {
|
|
link.classList.remove('active');
|
|
if (link.getAttribute('data-section') === id) {
|
|
link.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.3,
|
|
rootMargin: '-20% 0px -60% 0px'
|
|
});
|
|
|
|
sections.forEach(function(section) {
|
|
observer.observe(section);
|
|
});
|
|
}
|
|
|
|
/* =========================================
|
|
SKILL GAUGES: Scroll-triggered animation
|
|
========================================= */
|
|
|
|
function initSkillGauges() {
|
|
var skillsSection = document.getElementById('skills');
|
|
if (!skillsSection) return;
|
|
|
|
var skillItems = skillsSection.querySelectorAll('.skill-item');
|
|
var circumference = 2 * Math.PI * 34; // ~213.628
|
|
var animated = false;
|
|
|
|
var gaugeObserver = new IntersectionObserver(function(entries) {
|
|
entries.forEach(function(entry) {
|
|
if (entry.isIntersecting && !animated) {
|
|
animated = true;
|
|
gaugeObserver.unobserve(entry.target);
|
|
|
|
skillItems.forEach(function(item, index) {
|
|
var level = parseInt(item.getAttribute('data-level'), 10);
|
|
var progressCircle = item.querySelector('.skill-progress');
|
|
if (!progressCircle) return;
|
|
|
|
var targetOffset = circumference * (1 - level / 100);
|
|
|
|
// Stagger each gauge by 100ms
|
|
setTimeout(function() {
|
|
progressCircle.style.strokeDashoffset = targetOffset;
|
|
}, index * 100);
|
|
});
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.15
|
|
});
|
|
|
|
gaugeObserver.observe(skillsSection);
|
|
}
|
|
|
|
/* =========================================
|
|
SCROLL REVEAL: Sections & staggered children
|
|
========================================= */
|
|
|
|
function initScrollReveal() {
|
|
var sections = document.querySelectorAll('.cv-main section');
|
|
|
|
var revealObserver = new IntersectionObserver(function(entries) {
|
|
entries.forEach(function(entry) {
|
|
if (entry.isIntersecting) {
|
|
var section = entry.target;
|
|
section.classList.add('visible');
|
|
|
|
// Stagger child cards/items
|
|
var children = section.querySelectorAll(
|
|
'.vital-card, .skill-item, .timeline-entry, .education-card, .project-card, .contact-item'
|
|
);
|
|
children.forEach(function(child, i) {
|
|
child.style.transitionDelay = (i * 60) + 'ms';
|
|
});
|
|
|
|
// Only animate once — stop observing after reveal
|
|
revealObserver.unobserve(section);
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.15
|
|
});
|
|
|
|
sections.forEach(function(section) {
|
|
// Hero is always visible (above the fold), skip it
|
|
if (section.classList.contains('hero')) {
|
|
section.classList.add('visible');
|
|
return;
|
|
}
|
|
revealObserver.observe(section);
|
|
});
|
|
}
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|