Restyle CV: navy accent system + competency-first structure

Rework the résumé as an HR-first document. Move Kernkompetenzen (renamed
from Fähigkeiten) directly under the Profil so the role-match is visible
in the first seconds, ahead of the reverse-chronological experience.

Introduce one restrained deep navy-slate accent used only for structure:
the name, section labels, a short accent bar starting each section rule,
and small square bullet markers — a coherent, precise motif. Body text
stays near-black for a crisp, ATS-safe read. Verified on the with-photo
and no-photo layouts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 17:58:22 +02:00
co-authored by Claude Opus 4.8
parent 05357acddb
commit ddcf5edbc3
+33 -14
View File
@@ -596,24 +596,36 @@ const RC = {
ink: [26, 26, 26],
sub: [92, 98, 106],
hair: [206, 208, 212],
// One deep, professional navy-slate accent — used sparingly for structure
// (name, section labels, the short header rule, bullet markers). It signals
// care and confidence without ever reading as flashy; body text stays near
// black so the résumé remains crisp and ATS-safe.
accent: [31, 64, 104],
};
// Section heading: bold uppercase tracked label with a thin hairline beneath.
// Section heading: an accent-coloured, tracked uppercase label over a full-width
// hairline, with a short heavier accent segment at the start — a small, precise
// motif that gives every section a confident, consistent anchor.
function cvHeading(doc, S, dry, cur, title, ruleRight) {
const pt = 11;
cur.y += 3 * S;
const pt = 10.5;
cur.y += 3.6 * S;
doc.setFont('Lato', 'bold');
doc.setFontSize(pt * S);
const lineY = cur.y + pt * S * PT * 1.08;
if (!dry) {
doc.setCharSpace(0.6 * S);
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
doc.setCharSpace(0.9 * S);
doc.setTextColor(RC.accent[0], RC.accent[1], RC.accent[2]);
doc.text(String(title).toUpperCase(), RV.mx, cur.y + pt * S * PT * 0.76);
doc.setCharSpace(0);
const right = ruleRight || RV_R;
doc.setDrawColor(RC.hair[0], RC.hair[1], RC.hair[2]);
doc.setLineWidth(0.3 * S);
doc.line(RV.mx, cur.y + pt * S * PT * 1.0, ruleRight || RV_R, cur.y + pt * S * PT * 1.0);
doc.line(RV.mx, lineY, right, lineY);
doc.setDrawColor(RC.accent[0], RC.accent[1], RC.accent[2]);
doc.setLineWidth(1.0 * S);
doc.line(RV.mx, lineY, Math.min(RV.mx + 15 * S, right), lineY);
}
cur.y += pt * S * PT + 3.4 * S;
cur.y += pt * S * PT + 4.0 * S;
}
// One row: title (left) + muted date (right-aligned) on a single baseline.
@@ -650,7 +662,12 @@ function cvBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
const lines = doc.splitTextToSize(String(it), tw);
lines.forEach((ln, i) => {
if (!dry) {
if (i === 0) { doc.setFillColor(RC.ink[0], RC.ink[1], RC.ink[2]); doc.circle(x + 0.8 * S, cur.y + pt * S * PT * 0.42, 0.6 * S, 'F'); }
if (i === 0) {
// Small accent square marker (echoes the section-label motif).
const sz = 1.15 * S;
doc.setFillColor(RC.accent[0], RC.accent[1], RC.accent[2]);
doc.rect(x + 0.2 * S, cur.y + pt * S * PT * 0.42 - sz / 2, sz, sz, 'F');
}
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
doc.text(ln, tx, cur.y + pt * S * PT * 0.76);
}
@@ -712,8 +729,8 @@ function composeCvHeader(doc, S, dry, cur, header, titel, foto) {
const gapPhoto = 6 * S;
const leftW = photoOk ? (RV_W - photoW - gapPhoto) : RV_W * 0.62;
write(doc, S, dry, cur, header.name, { pt: 22, style: 'bold', color: RC.ink, x: RV.mx, width: leftW, factor: 1.05 });
if (titel) write(doc, S, dry, cur, titel, { pt: 12, color: RC.sub, x: RV.mx, width: leftW, factor: 1.15 });
write(doc, S, dry, cur, header.name, { pt: 22, style: 'bold', color: RC.accent, x: RV.mx, width: leftW, factor: 1.08 });
if (titel) write(doc, S, dry, cur, titel, { pt: 11.5, color: RC.sub, x: RV.mx, width: leftW, factor: 1.32, charSpace: 0.2 });
if (header.geburtsdatum) write(doc, S, dry, cur, `Geburtsdatum: ${header.geburtsdatum}`, { pt: 9, color: RC.sub, x: RV.mx, width: leftW, factor: 1.3 });
if (photoOk) {
// contact details left-aligned under the name/title (photo takes the right)
@@ -789,6 +806,12 @@ function composeCV(doc, S, dry, cur, { cv, header, titel, foto }) {
} else {
cur.y = Math.max(cur.y, geo.photoBottom);
}
// Kernkompetenzen right after the Profil: the recruiter sees the role-match
// in the first seconds, before diving into the experience below.
if (cv.kenntnisse.length) {
cvHeading(doc, S, dry, cur, 'Kernkompetenzen');
cvTwoColBullets(doc, S, dry, cur, cv.kenntnisse, RV.mx, RV_W);
}
if (cv.berufserfahrung.length) {
cvHeading(doc, S, dry, cur, 'Berufserfahrung');
cv.berufserfahrung.forEach((e) => cvExperience(doc, S, dry, cur, e));
@@ -798,10 +821,6 @@ function composeCV(doc, S, dry, cur, { cv, header, titel, foto }) {
cvHeading(doc, S, dry, cur, t);
entries.forEach((e) => cvEducation(doc, S, dry, cur, e));
});
if (cv.kenntnisse.length) {
cvHeading(doc, S, dry, cur, 'Fähigkeiten');
cvTwoColBullets(doc, S, dry, cur, cv.kenntnisse, RV.mx, RV_W);
}
if (cv.sprachen.length) {
cvHeading(doc, S, dry, cur, 'Sprachen');
cv.sprachen.forEach((s) => cvRow(doc, S, dry, cur, s.sprache, s.niveau, 9.7, 'normal'));