From ddcf5edbc32bb8f43509655e0a6ed51173a963d5 Mon Sep 17 00:00:00 2001 From: Thomas Hackner Date: Fri, 3 Jul 2026 17:58:22 +0200 Subject: [PATCH] Restyle CV: navy accent system + competency-first structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/documents.js | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/documents.js b/lib/documents.js index d664b19..5cb04b4 100644 --- a/lib/documents.js +++ b/lib/documents.js @@ -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'));