Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [Unreleased]

### Changed (homepage v3: Technical Project Manager / Implementation Lead)
- Title everywhere is "Technical Project Manager / Implementation Lead" (same as the CV and LinkedIn): meta, OG and Twitter tags, hero, footer tagline and copyright line, OG image alt
- Hero: H1 is the name, the title sits directly under it, new subtitle, and two buttons: Download CV (download) and LinkedIn (new tab, `rel="noopener noreferrer"`). `PageHero` gets an optional `titleLine` prop
- Homepage renders Hero, Work (cards unchanged), About (new copy, photo, experience timeline, education/languages/location) and Contact. The credibility strip, Other builds, "How I run delivery" and the AI teaser are no longer rendered; their data stays in `src/content/site.js`
- Header nav is Work · About · AI · Contact on every page. The Services link shows only on `/services` (with its Book a call button). The footer drops the Services and GitHub links and gets a small "Consulting" link to `/services`
- Capstone is Rolevoice: the AI Field Guide card and the experience timeline no longer say DeutschOn-AI. `/ai` drops the teacher and student wording from the capstone entry and the hosted-tools practice
- Consistency guard: header comment updated; bans "System implementation in regulated environments", "Technical Project Manager for system implementation", "upskilling on purpose", "20+ years taking software" and "DeutschOn"

### Changed (CV PDF, Oct 2026)
- `public/roman-mazuryk-cv.pdf` replaced with the Oct 2026 CV: headline "Technical Project Manager / Implementation Lead", romazuryk@gmail.com, PharmaLogis 08/2024 to 11/2025, M.Sc. Computer Science, Rolevoice capstone. The May CV ("Technical Product Manager", proton.me email, P&L figure) is gone

### Fixed (footer contact spacing)
- Footer Contact column: the Download CV button and the email link now have a 20px gap side by side and a 12px gap when they wrap, on desktop and mobile

Expand Down
12 changes: 6 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="canonical" href="https://www.mazuryk.dev/">

<title>Roman Mazuryk · Technical Project Manager, System Implementation</title>
<title>Roman Mazuryk | Technical Project Manager / Implementation Lead</title>
<meta
name="description"
content="Technical project manager with 20+ years in MedTech, pharma and logistics. 20+ system projects from discovery to adoption. Hamburg / Remote (EU)."
content="Technical Project Manager and Implementation Lead. 20+ years in regulated MedTech, pharma and logistics, taking systems live since 2012, now adding hands-on AI. Hamburg area, open to remote across the EU."
>

<meta property="og:url" content="https://www.mazuryk.dev/">
<meta property="og:type" content="website">
<meta property="og:title" content="Roman Mazuryk · Technical Project Manager, System Implementation">
<meta property="og:description" content="Technical project manager with 20+ years in MedTech, pharma and logistics. 20+ system projects from discovery to adoption. Hamburg / Remote (EU).">
<meta property="og:title" content="Roman Mazuryk | Technical Project Manager / Implementation Lead">
<meta property="og:description" content="Technical Project Manager and Implementation Lead. 20+ years in regulated MedTech, pharma and logistics, taking systems live since 2012, now adding hands-on AI. Hamburg area, open to remote across the EU.">
<meta property="og:image" content="https://www.mazuryk.dev/images/og/og-home.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
Expand All @@ -29,8 +29,8 @@
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="mazuryk.dev">
<meta property="twitter:url" content="https://www.mazuryk.dev/">
<meta name="twitter:title" content="Roman Mazuryk · Technical Project Manager, System Implementation">
<meta name="twitter:description" content="Technical project manager with 20+ years in MedTech, pharma and logistics. 20+ system projects from discovery to adoption. Hamburg / Remote (EU).">
<meta name="twitter:title" content="Roman Mazuryk | Technical Project Manager / Implementation Lead">
<meta name="twitter:description" content="Technical Project Manager and Implementation Lead. 20+ years in regulated MedTech, pharma and logistics, taking systems live since 2012, now adding hands-on AI. Hamburg area, open to remote across the EU.">
<meta name="twitter:image" content="https://www.mazuryk.dev/images/og/og-home.png">
<style>
.app-shell-header {
Expand Down
Binary file modified public/roman-mazuryk-cv.pdf
Binary file not shown.
10 changes: 9 additions & 1 deletion scripts/check-consistency.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// wrong numbers, the old email address or em dashes creep back into the site.
//
// Why these are banned:
// - Identity is "Technical Project Manager, system implementation in regulated environments" (CV headline).
// - Identity is "Technical Project Manager / Implementation Lead" (CV and LinkedIn headline).
// Older role labels contradict the CV that recruiters download.
// - The M.Sc. capstone is Rolevoice. "DeutschOn" is its retired name and must not appear in site copy.
// - Years are 12+ (implementation) and 20+ (industry), as in the CV. "10+" and "13+" were earlier drafts.
// - Contact email is romazuryk@gmail.com, as in the CV. The proton.me address is retired.
// - No em dashes in site copy (src/content/site.js).
Expand All @@ -25,6 +26,13 @@ const BANNED_PHRASES = [
"at FlowLogics,",
"FlowLogics, Reinbek",
"FlowLogics, internal systems rollout",
// retired homepage identity and draft wording (homepage v3, Oct 2026)
"System implementation in regulated environments",
"Technical Project Manager for system implementation",
"upskilling on purpose",
"20+ years taking software",
// capstone is Rolevoice (D10)
"DeutschOn",
];

// Matched case-sensitively, unlike BANNED_PHRASES.
Expand Down
20 changes: 20 additions & 0 deletions src/assets/css/footer.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,26 @@
border-radius: 6px;
}

/* Small text link to /services: the only Consulting entry point outside /services. */
.footer__consulting-link {
width: fit-content;
color: var(--muted);
font-size: 12px;
font-weight: 600;
text-decoration: underline;
text-underline-offset: 3px;
}

.footer__consulting-link:hover {
color: var(--text);
}

.footer__consulting-link:focus-visible {
outline: 2px solid var(--accent-ai);
outline-offset: 2px;
border-radius: 4px;
}

/* ---- Bottom bar ---- */
.footer__bar {
border-top: 1px solid var(--border);
Expand Down
14 changes: 13 additions & 1 deletion src/assets/css/market-pages.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,19 @@
}

.market-page--home .page-hero__title {
margin-bottom: 28px;
margin-bottom: 14px;
}

/* Role title directly under the name (H1). */
.page-hero__title-line {
max-width: 980px;
margin: 0 0 24px;
color: var(--brand);
font-size: clamp(20px, 3vw, 30px);
font-weight: 750;
letter-spacing: -0.01em;
line-height: 1.25;
text-wrap: balance;
}

.market-page--home .page-hero__subtitle {
Expand Down
29 changes: 11 additions & 18 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from "react";
import { Linkedin, Github, Mail, FileDown, ArrowUpRight } from "lucide-react";
import { contact, identity, labels, nav, navAi, navServices } from "../content/site.js";
import { Linkedin, Mail, FileDown, ArrowUpRight } from "lucide-react";
import { contact, footer, identity, labels, nav } from "../content/site.js";

// Footer links per homepage v3: LinkedIn, email and CV. Consulting is a small text link in the Contact column.
const SOCIAL_LINKS = [
{ label: "LinkedIn", href: contact.linkedin, icon: Linkedin },
{ label: "GitHub", href: contact.github, icon: Github },
{ label: labels.linkedin, href: contact.linkedin, icon: Linkedin },
{ label: "Email", href: contact.emailHref, icon: Mail },
{ label: labels.downloadCv, href: contact.cvUrl, icon: FileDown, download: true },
];
Expand All @@ -13,8 +13,6 @@ export default function Footer() {
const year = new Date().getFullYear();
const path = typeof window !== "undefined" ? window.location.pathname.replace(/\/+$/, "") || "/" : "/";
const isHome = path === "/";
// Matches the header: no Services link on /ai.
const isAiPage = path === navAi.href;

return (
<footer className="site-footer">
Expand All @@ -32,7 +30,7 @@ export default function Footer() {
href={href}
className="footer__social"
target={href.startsWith("http") ? "_blank" : undefined}
rel={href.startsWith("http") ? "noreferrer" : undefined}
rel={href.startsWith("http") ? "noopener noreferrer" : undefined}
download={download || undefined}
aria-label={label}
title={label}
Expand All @@ -46,19 +44,11 @@ export default function Footer() {
<nav className="footer__nav-col" aria-label="Footer navigation">
<p className="footer__col-heading">{labels.navigation}</p>
<ul className="footer__nav">
{nav.map(({ id, label }) => (
<li key={id}>
<a href={isHome ? `#${id}` : `/#${id}`} className="footer__nav-link">{label}</a>
{nav.map(({ id, href, label }) => (
<li key={id || href}>
<a href={href || (isHome ? `#${id}` : `/#${id}`)} className="footer__nav-link">{label}</a>
</li>
))}
<li>
<a href={navAi.href} className="footer__nav-link">{navAi.label}</a>
</li>
{isAiPage ? null : (
<li>
<a href={navServices.href} className="footer__nav-link">{navServices.label}</a>
</li>
)}
</ul>
</nav>

Expand All @@ -70,6 +60,9 @@ export default function Footer() {
<a href={contact.emailHref} className="footer__secondary-link">
{contact.email} <ArrowUpRight size={13} aria-hidden="true" />
</a>
<a href={footer.consulting.href} className="footer__consulting-link">
{footer.consulting.label}
</a>
</div>
</div>

Expand Down
57 changes: 29 additions & 28 deletions src/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
import ThemeSwitcher from "./ThemeSwitcher.jsx";
import NavControls from "./NavControls.jsx";
import { identity, labels, nav, navAi, navServices } from "../content/site.js";
import { identity, labels, nav, navServices } from "../content/site.js";

// 800px matches the CSS mobile breakpoint
const MOBILE_MQ = "(max-width: 800px)";
Expand All @@ -18,11 +18,11 @@ function currentPath() {
export default function Navbar({ themeMode, onThemeChange }) {
const path = currentPath();
const isHome = path === "/";
// Header nav is Work · About · AI · Contact. The Services link appears only on /services
// itself; everywhere else Consulting is reachable from the footer.
const isServicesPage = path === navServices.href;
// /ai is written for hiring teams, so the Services link is hidden there.
const isAiPage = path === navAi.href;
// Anchors scroll on the landing page and link back to it from every other page.
const navIds = useMemo(() => (isHome ? nav.map((item) => item.id) : []), [isHome]);
const navIds = useMemo(() => (isHome ? nav.filter((item) => item.id).map((item) => item.id) : []), [isHome]);
// Exception: on /services, Contact points to that page's own #contact section, because the
// landing page's contact block is written for recruiters. It is not highlighted on scroll,
// so Services stays the only active item there.
Expand Down Expand Up @@ -156,40 +156,41 @@ export default function Navbar({ themeMode, onThemeChange }) {
className={`nav__list ${isOpen ? "nav__list--open" : ""}`}
inert={isMobile && !isOpen ? "" : undefined}
>
{nav.map(({ id, label }) => (
<li key={id}>
<a
href={isLocal(id) ? `#${id}` : `/#${id}`}
className={`nav__link ${isHome && active === id ? "nav__link--active" : ""}`}
aria-current={isHome && active === id ? "location" : undefined}
onClick={() => handleClick(id)}
>
{label}
</a>
{nav.map(({ id, href, label }) => (
<li key={id || href}>
{id ? (
<a
href={isLocal(id) ? `#${id}` : `/#${id}`}
className={`nav__link ${isHome && active === id ? "nav__link--active" : ""}`}
aria-current={isHome && active === id ? "location" : undefined}
onClick={() => handleClick(id)}
>
{label}
</a>
) : (
<a
href={href}
className={`nav__link ${path === href ? "nav__link--active" : ""}`}
aria-current={path === href ? "page" : undefined}
onClick={() => setIsOpen(false)}
>
{label}
</a>
)}
</li>
))}
<li>
<a
href={navAi.href}
className={`nav__link ${isAiPage ? "nav__link--active" : ""}`}
aria-current={isAiPage ? "page" : undefined}
onClick={() => setIsOpen(false)}
>
{navAi.label}
</a>
</li>
{isAiPage ? null : (
{isServicesPage ? (
<li>
<a
href={navServices.href}
className={`nav__link ${isServicesPage ? "nav__link--active" : ""}`}
aria-current={isServicesPage ? "page" : undefined}
className="nav__link nav__link--active"
aria-current="page"
onClick={() => setIsOpen(false)}
>
{navServices.label}
</a>
</li>
)}
) : null}
</ul>

<div className="nav__actions">
Expand Down
4 changes: 3 additions & 1 deletion src/components/common/PageHero.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function CtaLink({ cta, fallbackClass }) {
href={href}
className={classes}
target={external ? "_blank" : undefined}
rel={external ? "noreferrer" : undefined}
rel={external ? "noopener noreferrer" : undefined}
download={download}
>
{label} {icon || null}
Expand All @@ -30,6 +30,7 @@ function CtaLink({ cta, fallbackClass }) {
export default function PageHero({
eyebrow,
title,
titleLine,
subtitle,
primaryCta,
secondaryCta,
Expand All @@ -48,6 +49,7 @@ export default function PageHero({
<div className="page-hero__content reveal">
{eyebrow ? <p className="page-hero__eyebrow">{eyebrow}</p> : null}
<h1 className="page-hero__title">{title}</h1>
{titleLine ? <p className="page-hero__title-line">{titleLine}</p> : null}
{subtitle ? <p className="page-hero__subtitle">{subtitle}</p> : null}

{(primaryCta || secondaryCta || tertiaryCta) ? (
Expand Down
Loading
Loading