openvibers/afresume Commits

PDF page-1 rework + nav fixes: one-line name/tagline, full role row, distinct contact bar, tighter cards, 2-line metrics, inline company names; glass PDF button; drawer bleed line

OP OpenVibers committed 20 Sept 2026 · 0ba58b2a53 · 28 files, +123 −77

PDF (resume/build_resume.py, profile.json)
- Header: tagline sits to the right of the name, both with an offset
  shadow; all ten role chips fill the banner width; contact info moves
  into its own darker band as white pills with dark text, so it reads as
  a separate row rather than more chips.
- Feature cards: less padding and smaller icon so each detail can run to
  two full lines; seeking[].detail rewritten to use them.
- Metrics strip: numbers smaller, labels larger and capped at two lines
  via shorter metrics[].short values.
- Experience: company · location join the role title line when they
  fit, saving a line per role; the freed space went into larger body,
  bullet, skills and project type on both pages.

Site (Nav.astro)
- The closed mobile drawer showed as a 2px dark line under the nav (its
  background bled through a transparent border at max-height 0); it is
  now visibility-hidden when closed.
- PDF button is an accent-tinted glass pill with an outline and the
  page's text color, so it stays readable on pink/lime/yellow accents
  and re-tints with the theme.

Changes

ddiff --git a/public/Alex_Frison_Resume_Sep_2026_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Sep_2026_1c4cdbb2.pdf
ddeleted file mode 100644
iindex bef2170..0000000
BBinary files a/public/Alex_Frison_Resume_Sep_2026_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Sep_2026_76413598.pdf b/public/Alex_Frison_Resume_Sep_2026_76413598.pdf
nnew file mode 100644
iindex 0000000..7034b38
BBinary files /dev/null and b/public/Alex_Frison_Resume_Sep_2026_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_amber_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_amber_1c4cdbb2.pdf
ddeleted file mode 100644
iindex 8cd3e9a..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_amber_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_amber_76413598.pdf b/public/Alex_Frison_Resume_Themed_amber_76413598.pdf
nnew file mode 100644
iindex 0000000..c12a81f
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_amber_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_cyan_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_cyan_1c4cdbb2.pdf
ddeleted file mode 100644
iindex 10defd8..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_cyan_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_cyan_76413598.pdf b/public/Alex_Frison_Resume_Themed_cyan_76413598.pdf
nnew file mode 100644
iindex 0000000..b45ffc2
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_cyan_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_green_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_green_1c4cdbb2.pdf
ddeleted file mode 100644
iindex f4d433d..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_green_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_green_76413598.pdf b/public/Alex_Frison_Resume_Themed_green_76413598.pdf
nnew file mode 100644
iindex 0000000..ecfd34d
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_green_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_indigo_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_indigo_1c4cdbb2.pdf
ddeleted file mode 100644
iindex cfd189f..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_indigo_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_indigo_76413598.pdf b/public/Alex_Frison_Resume_Themed_indigo_76413598.pdf
nnew file mode 100644
iindex 0000000..e4b6959
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_indigo_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_lime_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_lime_1c4cdbb2.pdf
ddeleted file mode 100644
iindex b0e1417..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_lime_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_lime_76413598.pdf b/public/Alex_Frison_Resume_Themed_lime_76413598.pdf
nnew file mode 100644
iindex 0000000..bb23e85
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_lime_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_orange_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_orange_1c4cdbb2.pdf
ddeleted file mode 100644
iindex 53ecc95..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_orange_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_orange_76413598.pdf b/public/Alex_Frison_Resume_Themed_orange_76413598.pdf
nnew file mode 100644
iindex 0000000..58e5faa
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_orange_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_pink_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_pink_1c4cdbb2.pdf
ddeleted file mode 100644
iindex d0243d6..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_pink_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_pink_76413598.pdf b/public/Alex_Frison_Resume_Themed_pink_76413598.pdf
nnew file mode 100644
iindex 0000000..cd29a8d
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_pink_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_red_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_red_1c4cdbb2.pdf
ddeleted file mode 100644
iindex 07bc4e2..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_red_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_red_76413598.pdf b/public/Alex_Frison_Resume_Themed_red_76413598.pdf
nnew file mode 100644
iindex 0000000..6a71bb0
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_red_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_teal_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_teal_1c4cdbb2.pdf
ddeleted file mode 100644
iindex cfccde7..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_teal_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_teal_76413598.pdf b/public/Alex_Frison_Resume_Themed_teal_76413598.pdf
nnew file mode 100644
iindex 0000000..92041a3
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_teal_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_violet_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_violet_1c4cdbb2.pdf
ddeleted file mode 100644
iindex 844f4a3..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_violet_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_violet_76413598.pdf b/public/Alex_Frison_Resume_Themed_violet_76413598.pdf
nnew file mode 100644
iindex 0000000..8ad52b3
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_violet_76413598.pdf differ
ddiff --git a/public/Alex_Frison_Resume_Themed_yellow_1c4cdbb2.pdf b/public/Alex_Frison_Resume_Themed_yellow_1c4cdbb2.pdf
ddeleted file mode 100644
iindex 80c5c36..0000000
BBinary files a/public/Alex_Frison_Resume_Themed_yellow_1c4cdbb2.pdf and /dev/null differ
ddiff --git a/public/Alex_Frison_Resume_Themed_yellow_76413598.pdf b/public/Alex_Frison_Resume_Themed_yellow_76413598.pdf
nnew file mode 100644
iindex 0000000..ace25fb
BBinary files /dev/null and b/public/Alex_Frison_Resume_Themed_yellow_76413598.pdf differ
ddiff --git a/resume/build_resume.py b/resume/build_resume.py
iindex 1fd8435..5361671 100644
---- a/resume/build_resume.py
++++ b/resume/build_resume.py
@@@ -321,19 +321,19 @@ def style(name: str, size: float, leading: float, color, font: str | None = None
class Styles:
def __init__(self) -> None:
-- self.body = style("body", 8.7, 10.8, BODY)
-- self.summary = style("summary", 8.1, 9.8, MUTED, FONT["italic"])
-- self.bullet = style("bullet", 8.2, 9.9, BODY, leftIndent=9, bulletIndent=0,
++ self.body = style("body", 9.1, 11.4, BODY)
++ self.summary = style("summary", 8.4, 10.3, MUTED, FONT["italic"])
++ self.bullet = style("bullet", 8.8, 10.9, BODY, leftIndent=9, bulletIndent=0,
bulletFontName=FONT["regular"], bulletFontSize=8.2, bulletColor=BLUE)
-- self.bullet_compact = style("bulletc", 8.2, 10.0, BODY, leftIndent=9, bulletIndent=0,
++ self.bullet_compact = style("bulletc", 8.6, 10.7, BODY, leftIndent=9, bulletIndent=0,
bulletFontName=FONT["regular"], bulletFontSize=8.2, bulletColor=BLUE)
self.stack = style("stack", 6.9, 8.3, MUTED)
self.metric_label = style("mlabel", 7.0, 8.5, MUTED)
self.proj_title = style("ptitle", 8.5, 10.2, INK, FONT["bold"])
-- self.proj_summary = style("psum", 7.5, 9.0, BODY)
++ self.proj_summary = style("psum", 7.9, 9.6, BODY)
self.proj_link = style("plink", 7.3, 8.8, BLUE)
-- self.skill_items = style("skills", 8.2, 9.7, BODY)
-- self.edu = style("edu", 8.3, 10.1, BODY)
++ self.skill_items = style("skills", 8.5, 10.3, BODY)
++ self.edu = style("edu", 8.7, 10.8, BODY)
self.chip = style("chip", 7.6, 9.2, BLUE_DARK, FONT["bold"])
self.hero_line = style("heroline", 8.6, 11.2, white)
@@@ -536,11 +536,15 @@ def block_header(c, profile: dict, st: Styles, y_top: float, draw: bool) -> floa
cx += cw + gap
return rows
-- def draw_rows(rows, row_top, chip_h, font, size, icon_w, pad_x, gap, bg, bg_alpha, fg, glyph_of):
++ def draw_rows(rows, row_top, chip_h, font, size, icon_w, pad_x, gap, bg, bg_alpha, fg, glyph_of, icon_fg=None, shadow=False):
for row in rows:
cx2 = x + pad
for it, cw in row:
c.saveState()
++ if shadow:
++ c.setFillColor(BLUE_DARK)
++ c.setFillAlpha(0.22)
++ c.roundRect(cx2, row_top - chip_h - 1.6, cw, chip_h, chip_h / 2, stroke=0, fill=1)
c.setFillColor(bg)
c.setFillAlpha(bg_alpha)
c.roundRect(cx2, row_top - chip_h, cw, chip_h, chip_h / 2, stroke=0, fill=1)
@@@ -549,7 +553,7 @@ def block_header(c, profile: dict, st: Styles, y_top: float, draw: bool) -> floa
glyph = glyph_of(it)
tx = cx2 + pad_x / 2
if glyph:
-- icon(c, tx, ty2, glyph, size, fg)
++ icon(c, tx, ty2, glyph, size, icon_fg or fg)
tx += icon_w
c.setFillColor(fg)
c.setFont(font, size)
@@@ -560,7 +564,7 @@ def block_header(c, profile: dict, st: Styles, y_top: float, draw: bool) -> floa
row_top -= chip_h + 5
return row_top
-- roles = [{"text": r, "url": None} for r in (profile.get("roles") or [])[:6]]
++ roles = [{"text": r, "url": None} for r in (profile.get("roles") or [])]
role_font, role_size, role_pad_x, role_icon_w, role_gap = FONT["bold"], 7.6, 20.0, 13.0, 7.0
role_rows = wrap(roles, role_font, role_size, role_icon_w, role_pad_x, role_gap)
role_h = role_size + 6.5
@@@ -574,13 +578,18 @@ def block_header(c, profile: dict, st: Styles, y_top: float, draw: bool) -> floa
{"text": profile.get("locationShort") or profile.get("location"), "icon": "location", "url": None},
]
contact_items = [it for it in contact_items if it["text"]]
-- contact_font, contact_size, contact_pad_x, contact_icon_w, contact_gap = FONT["regular"], 8.6, 18.0, 15.0, 8.0
++ contact_font, contact_size, contact_pad_x, contact_icon_w, contact_gap = FONT["bold"], 8.4, 18.0, 15.0, 8.0
contact_rows = wrap(contact_items, contact_font, contact_size, contact_icon_w, contact_pad_x, contact_gap)
-- contact_h = contact_size + 7
++ contact_h = contact_size + 8
contact_total_h = len(contact_rows) * contact_h + max(0, len(contact_rows) - 1) * 6
-- group_gap = 6.0
-- banner_h = pad + name_size * 0.78 + 5 + tag_size * 0.78 + 13 + role_total_h + group_gap + contact_total_h + pad
++ # The contact row sits in its own darker band along the bottom of the banner, as white
++ # pills with dark text — the inverse of the translucent role chips above it, so the two rows
++ # read as "what I am" and "how to reach me" instead of one undifferentiated pile of pills.
++ band_pad = 8.0
++ band_h = contact_total_h + band_pad * 2
++ group_gap = 9.0
++ banner_h = pad + name_size * 0.78 + 10 + role_total_h + group_gap + band_h
if draw:
c.saveState()
@@@ -596,22 +605,33 @@ def block_header(c, profile: dict, st: Styles, y_top: float, draw: bool) -> floa
ty = y_top - pad
if draw:
++ # Name and tagline share one line; both get a soft offset shadow (reportlab has no blur,
++ # so a translucent dark copy one point down-right is the closest thing).
++ base = ty - name_size * 0.78
++ name_w = pdfmetrics.stringWidth(profile["name"], FONT["bold"], name_size)
++ c.saveState()
++ c.setFillColor(BLUE_DARK); c.setFillAlpha(0.45)
++ c.setFont(FONT["bold"], name_size); c.drawString(x + pad + 1.2, base - 1.4, profile["name"])
++ c.setFont(FONT["regular"], tag_size); c.drawString(x + pad + name_w + 14 + 0.8, base + 0.6 - 1.0, profile["tagline"])
++ c.restoreState()
c.setFillColor(white)
c.setFont(FONT["bold"], name_size)
-- c.drawString(x + pad, ty - name_size * 0.78, profile["name"])
-- ty -= name_size * 0.78 + 5
-- if draw:
-- c.setFillColor(HexColor("#dbe4ff"))
++ c.drawString(x + pad, base, profile["name"])
++ c.setFillColor(HexColor("#e4eaff"))
c.setFont(FONT["regular"], tag_size)
-- c.drawString(x + pad, ty - tag_size * 0.78, profile["tagline"])
-- ty -= tag_size * 0.78 + 10
++ c.drawString(x + pad + name_w + 14, base + 0.6, profile["tagline"])
++ ty -= name_size * 0.78 + 10
if draw:
ty = draw_rows(role_rows, ty, role_h, role_font, role_size, role_icon_w, role_pad_x, role_gap,
BLUE_SOFT, 0.4, white, lambda it: role_icon(it["text"]))
-- ty -= group_gap - 5
-- draw_rows(contact_rows, ty, contact_h, contact_font, contact_size, contact_icon_w, contact_pad_x, contact_gap,
-- white, 0.16, white, lambda it: ICON[it["icon"]])
++ c.saveState()
++ p = c.beginPath(); p.roundRect(x, y_top - banner_h, w, banner_h, 12); c.clipPath(p, stroke=0, fill=0)
++ c.setFillColor(BLUE_DARK); c.setFillAlpha(0.22)
++ c.rect(x, y_top - banner_h, w, band_h, stroke=0, fill=1)
++ c.restoreState()
++ draw_rows(contact_rows, y_top - banner_h + band_h - band_pad, contact_h, contact_font, contact_size, contact_icon_w, contact_pad_x, contact_gap,
++ white, 0.94, BLUE_DARK, lambda it: ICON[it["icon"]], icon_fg=BLUE, shadow=True)
return banner_h
@@@ -630,13 +650,13 @@ def block_feature_cards(c, seeking: list[dict], st: Styles, y_top: float, draw:
rows = [seeking[i:i + cols] for i in range(0, n, cols)]
gap = 8.0
w = (CONTENT_W - gap * (cols - 1)) / cols
-- pad = 8.5
-- icon_r = 11.0
++ pad = 6.5
++ icon_r = 8.5
inner_w = w - 2 * pad
-- title_w = inner_w - icon_r * 2 - 10
-- title_st = ParagraphStyle("fctitle", fontName=FONT["bold"], fontSize=9.6, leading=1, textColor=INK)
-- detail_st = ParagraphStyle("fcdetail", fontName=FONT["regular"], fontSize=7.6, leading=9.4, textColor=MUTED,
-- bulletFontName=FONT["regular"], bulletFontSize=7.6, bulletColor=BLUE)
++ title_w = inner_w - icon_r * 2 - 8
++ title_st = ParagraphStyle("fctitle", fontName=FONT["bold"], fontSize=9.4, leading=1, textColor=INK)
++ detail_st = ParagraphStyle("fcdetail", fontName=FONT["regular"], fontSize=7.8, leading=9.6, textColor=BODY,
++ bulletFontName=FONT["regular"], bulletFontSize=7.8, bulletColor=BLUE)
prepared = []
for s in seeking:
title_text = s["label"]
@@@ -645,12 +665,12 @@ def block_feature_cards(c, seeking: list[dict], st: Styles, y_top: float, draw:
detail_p = para(esc(s.get("detail", "")), detail_st, bullet="•")
prepared.append((title_text, detail_p))
title_row_h = max(icon_r * 2, title_st.fontSize * 1.2)
-- row_gap = 7.0
++ row_gap = 6.0
row_heights = []
for r in range(len(rows)):
chunk = prepared[r * cols:(r + 1) * cols]
detail_h = max(para_height(p, inner_w) for _, p in chunk)
-- row_heights.append(pad + title_row_h + 9 + detail_h + pad)
++ row_heights.append(pad + title_row_h + 7 + detail_h + pad)
h = sum(row_heights) + row_gap * (len(rows) - 1)
if draw:
row_top = y_top
@@@ -664,13 +684,13 @@ def block_feature_cards(c, seeking: list[dict], st: Styles, y_top: float, draw:
card(c, x, row_top, w, rh, radius=12)
top = row_top - pad
icon_cy = top - title_row_h / 2
-- icon_circle(c, x + pad + icon_r, icon_cy, icon_r, seeking_icon(s["label"]), BLUE, ICON_BG, 10.6)
++ icon_circle(c, x + pad + icon_r, icon_cy, icon_r, seeking_icon(s["label"]), BLUE, ICON_BG, 8.6)
c.setFillColor(INK)
c.setFont(title_st.fontName, title_st.fontSize)
-- c.drawString(x + pad + icon_r * 2 + 10, icon_cy - title_st.fontSize * 0.36, title_text)
-- rule_y = top - title_row_h - 6
++ c.drawString(x + pad + icon_r * 2 + 8, icon_cy - title_st.fontSize * 0.36, title_text)
++ rule_y = top - title_row_h - 4
hairline(c, x + pad, rule_y, x + w - pad)
-- draw_para(c, detail_p, x + pad, rule_y - 8, inner_w)
++ draw_para(c, detail_p, x + pad, rule_y - 4, inner_w)
row_top -= rh + row_gap
return h
@@@ -681,14 +701,14 @@ def block_metrics(c, metrics: list[dict], st: Styles, y_top: float, draw: bool)
separate boxed cards."""
n = len(metrics)
col_w = CONTENT_W / n
-- icon_r = 11.0
-- num_size = 15.0
-- pad = 10.0
-- label_st = ParagraphStyle("mlabel2", fontName=FONT["regular"], fontSize=7.1, leading=8.5, textColor=MUTED)
++ icon_r = 9.5
++ num_size = 12.5
++ pad = 9.0
++ label_st = ParagraphStyle("mlabel2", fontName=FONT["regular"], fontSize=7.5, leading=8.9, textColor=BODY)
text_w = col_w - pad - icon_r * 2 - 8 - 6
label_paras = [para(esc(m.get("short") or m["label"]), label_st) for m in metrics]
label_h = max(para_height(p, text_w) for p in label_paras)
-- h = max(icon_r * 2 + 12, num_size + label_h + 6) + 12
++ h = max(icon_r * 2 + 10, num_size + label_h + 4) + 10
if draw:
card(c, MARGIN, y_top, CONTENT_W, h, radius=12)
cy = y_top - h / 2
@@@ -734,6 +754,13 @@ def role_header(c, role: dict, st: Styles, x: float, w: float, y_top: float, dra
dates = f"{role.get('start', '')} – {role.get('end', '')}".strip(" –")
dw = pdfmetrics.stringWidth(dates, FONT["regular"], date_size)
base = y - title_size * 0.78
++ title_w = pdfmetrics.stringWidth(role["role"], FONT["bold"], title_size)
++ company_w = pdfmetrics.stringWidth(role["company"], FONT["bold"], meta_size)
++ loc = f" · {role['location']}" if role.get("location") else ""
++ loc_w = pdfmetrics.stringWidth(loc, FONT["regular"], meta_size)
++ # Company (and location) join the title line when there's room; otherwise they take a
++ # second line as before. Saves a line per role on most of the sheet.
++ inline = x + icon_col + title_w + 10 + company_w + loc_w < x + w - dw - 14
if draw:
glyph = KIND_ICON.get(role.get("kind"), DEFAULT_ICON)
icon_circle(c, x + 6, base + title_size * 0.28, 7.4, glyph, BLUE, ICON_BG, 7.2)
@@@ -746,13 +773,20 @@ def role_header(c, role: dict, st: Styles, x: float, w: float, y_top: float, dra
if role.get("current") or str(role.get("end", "")).lower() == "present":
c.setFillColor(MINT)
c.circle(x + w - dw - 7.5, base + 3.4, 2.1, stroke=0, fill=1)
-- y = base - 2.6
-- base = y - meta_size * 0.78
++ if inline:
++ cx0 = x + icon_col + title_w + 10
++ meta_base = base + 0.4
++ else:
++ y = base - 2.6
++ meta_base = y - meta_size * 0.78
++ cx0 = x + icon_col
++ base = meta_base
if draw:
c.setFillColor(BLUE)
c.setFont(FONT["bold"], meta_size)
-- c.drawString(x + icon_col, base, role["company"])
-- cw = pdfmetrics.stringWidth(role["company"], FONT["bold"], meta_size)
++ c.drawString(cx0, base, role["company"])
++ cw = company_w
++ x_save, x = x, cx0 - icon_col # keep the link/underline math below relative to cx0
if role.get("link"):
# A thin underline is the only cue a static PDF has for "this text is a live
# link" — plain bold-blue looks identical to every other (non-linked) company name.
@@@ -766,7 +800,8 @@ def role_header(c, role: dict, st: Styles, x: float, w: float, y_top: float, dra
c.setFillColor(MUTED)
c.setFont(FONT["regular"], meta_size)
c.drawString(x + icon_col + cw, base, f" · {role['location']}")
-- y = base - 2.0
++ x = x_save
++ y = base - (2.0 if not inline else 3.0)
return y_top - y
ddiff --git a/src/components/Nav.astro b/src/components/Nav.astro
iindex e66501f..317ab8e 100644
---- a/src/components/Nav.astro
++++ b/src/components/Nav.astro
@@@ -166,13 +166,21 @@ const path = Astro.url.pathname;
}
.nav__end { margin-left: auto; display: flex; align-items: center; gap: 6px; }
++ /* Glass pill tinted by the accent, with the page's own text color on top — readable on any
++ accent (a solid bright pink with white text was not) and it re-tints itself as the theme changes. */
.nav__cta {
-- display: inline-flex; align-items: center; gap: 7px; padding: 8px 14px; border-radius: 999px;
-- background: var(--accent); color: var(--on-accent); font: 600 .82rem var(--font-body); text-decoration: none;
-- box-shadow: 0 8px 22px -10px rgba(var(--accent-rgb), .9);
-- transition: transform .3s var(--ease), box-shadow .3s var(--ease), filter .3s;
++ position: relative; display: inline-flex; align-items: center; gap: 8px; padding: 9px 15px 9px 12px; border-radius: 999px;
++ background: linear-gradient(180deg, rgba(var(--accent-rgb), .30), rgba(var(--accent-rgb), .16));
++ border: 1px solid rgba(var(--accent-rgb), .6); color: var(--fg);
++ font: 700 .8rem var(--font-body); letter-spacing: .02em; text-decoration: none;
++ backdrop-filter: blur(10px) saturate(140%); -webkit-backdrop-filter: blur(10px) saturate(140%);
++ box-shadow: inset 0 1px 0 rgba(255,255,255,.22), inset 0 -1px 0 rgba(0,0,0,.15), 0 0 0 3px rgba(var(--accent-rgb), .10), 0 10px 24px -12px rgba(var(--accent-rgb), .8);
++ transition: transform .3s var(--ease), box-shadow .3s var(--ease), background .3s var(--ease), border-color .3s var(--ease);
}
-- .nav__cta:hover { text-decoration: none; transform: translateY(-1px); filter: brightness(1.08); }
++ .nav__cta i { color: var(--accent); filter: brightness(1.25) saturate(1.1); font-size: .85rem; transition: transform .3s var(--ease); }
++ .nav__cta:hover { text-decoration: none; transform: translateY(-1px); border-color: rgba(var(--accent-rgb), .9); background: linear-gradient(180deg, rgba(var(--accent-rgb), .42), rgba(var(--accent-rgb), .22)); box-shadow: inset 0 1px 0 rgba(255,255,255,.28), 0 0 0 4px rgba(var(--accent-rgb), .14), 0 12px 28px -12px rgba(var(--accent-rgb), .95); }
++ .nav__cta:hover i { transform: translateY(2px); }
++ :root[data-theme="light"] .nav__cta { background: linear-gradient(180deg, rgba(var(--accent-rgb), .22), rgba(var(--accent-rgb), .12)); }
.nav__icon {
position: relative; width: 40px; height: 40px; border-radius: 50%; display: inline-grid; place-items: center;
background: transparent; border: 1px solid var(--border); color: var(--muted); cursor: pointer;
@@@ -200,11 +208,14 @@ const path = Astro.url.pathname;
max-width: var(--container); margin: 8px auto 0;
border-radius: var(--radius-lg); overflow: hidden;
background: color-mix(in oklab, var(--bg) 84%, transparent);
-- border: 1px solid transparent;
++ border: 1px solid transparent; background-clip: padding-box;
backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px);
-- max-height: 0; transition: max-height .45s var(--ease), border-color .3s;
++ /* Fully hidden when closed: at max-height 0 the 1px transparent border still let the dark
++ background show through as a 2px line under the nav. */
++ max-height: 0; visibility: hidden;
++ transition: max-height .45s var(--ease), border-color .3s, visibility 0s .45s;
}
-- .nav__drawer.is-open { max-height: 700px; border-color: var(--border); }
++ .nav__drawer.is-open { max-height: 700px; border-color: var(--border); visibility: visible; transition: max-height .45s var(--ease), border-color .3s, visibility 0s; }
.nav__drawer ul { list-style: none; margin: 0; padding: 10px; display: flex; flex-direction: column; gap: 3px; }
.nav__drawer li { opacity: 0; transform: translateY(8px); transition: opacity .4s var(--ease), transform .4s var(--ease); transition-delay: calc(var(--i) * 40ms); }
.nav__drawer.is-open li { opacity: 1; transform: none; }
ddiff --git a/src/data/profile.json b/src/data/profile.json
iindex a3e79c7..ef775ab 100644
---- a/src/data/profile.json
++++ b/src/data/profile.json
@@@ -36,7 +36,7 @@
"githubDisplay": "github.com/OpenVibers",
"linkedin": "https://www.linkedin.com/in/fris/",
"linkedinDisplay": "linkedin.com/in/fris",
-- "resumePdf": "/Alex_Frison_Resume_Sep_2026_1c4cdbb2.pdf",
++ "resumePdf": "/Alex_Frison_Resume_Sep_2026_76413598.pdf",
"availability": "Open to pretty much anything — engineering, platform, analytics, ops, sales, whatever fits · remote or Pacific Northwest",
"status": {
"now": "Contract software engineer at PowerChat, building its developer platform and payments program.",
@@@ -44,22 +44,22 @@
},
"pitch": "I'm a software engineer and business owner who builds complete products and keeps them running: websites, platforms, payment systems, AI agents and the automation behind them. Over the last decade I've shipped 25+ sites and services, rebuilt a live payments program across five processors, led a sales team, and turned an Amazon data-analysis job into tooling other analysts used every day. Give me a problem or a rough idea and I'll deliver the whole thing: design, code, deployment and support.",
"seeking": [
-- { "label": "Honestly, open to anything", "detail": "Title and level are negotiable — the team and the problem matter more" },
-- { "label": "Software engineering", "detail": "Full-stack, platform, streaming or realtime systems work" },
-- { "label": "Data & analytics", "detail": "SQL, reporting and compliance tooling people act on" },
-- { "label": "Ops, support, sales — whatever fits", "detail": "Ten years of sales and running my own company" }
++ { "label": "Honestly, open to anything", "detail": "Title and level are negotiable. The team, the problem and the chance to own something matter more than the label." },
++ { "label": "Software engineering", "detail": "Full-stack web apps, platforms, APIs, payments, streaming and realtime systems, designed, shipped and kept running in production." },
++ { "label": "Data & analytics", "detail": "SQL, Excel and Power Query, dashboards and compliance reporting people actually act on, plus automation that removes the busywork." },
++ { "label": "Ops, support, sales — whatever fits", "detail": "Ten years running my own company and three leading a retail sales team: I can talk to customers as comfortably as to servers." }
],
"foundedOrg": "Own company, LLC",
"lastRole": "PowerChat — Software Engineer (Contract)",
"metrics": [
-- { "num": 25, "suffix": "+", "label": "Websites, apps and services designed, built and shipped end to end", "short": "Sites, apps and services shipped end to end", "icon": "fa-solid fa-cubes-stacked" },
++ { "num": 25, "suffix": "+", "label": "Websites, apps and services designed, built and shipped end to end", "short": "Sites and services shipped", "icon": "fa-solid fa-cubes-stacked" },
{ "num": 500, "suffix": "k+", "label": "Lines of production code written and maintained", "short": "Lines of production code", "icon": "fa-solid fa-code" },
-- { "num": 900, "suffix": "+", "label": "API endpoints and pages running in production, all built solo", "short": "Endpoints and pages live in production", "icon": "fa-solid fa-route" },
++ { "num": 900, "suffix": "+", "label": "API endpoints and pages running in production, all built solo", "short": "Endpoints live in production", "icon": "fa-solid fa-route" },
{ "num": 169, "suffix": "", "label": "Free web tools built and published for anyone to use", "short": "Free web tools published", "icon": "fa-solid fa-toolbox" },
-- { "num": 5, "suffix": "", "label": "Payment processors merged into one billing system that can't double-charge", "short": "Payment processors unified into one billing system", "icon": "fa-solid fa-credit-card" },
-- { "num": 1000, "suffix": "+", "label": "Inventory discrepancies investigated and closed for SOX compliance at Amazon", "short": "SOX-compliance inventory cases closed at Amazon", "icon": "fa-solid fa-magnifying-glass-chart" },
-- { "num": 200, "suffix": "k+", "label": "Live game servers tracked by a platform I ran for a decade", "short": "Live game servers tracked by my platform", "icon": "fa-solid fa-tower-broadcast" },
-- { "num": 10, "suffix": "+", "label": "Years running my own company, from the code to the taxes", "short": "Years running my own company", "icon": "fa-solid fa-building-flag" }
++ { "num": 5, "suffix": "", "label": "Payment processors merged into one billing system that can't double-charge", "short": "Payment processors unified", "icon": "fa-solid fa-credit-card" },
++ { "num": 1000, "suffix": "+", "label": "Inventory discrepancies investigated and closed for SOX compliance at Amazon", "short": "SOX inventory cases closed", "icon": "fa-solid fa-magnifying-glass-chart" },
++ { "num": 200, "suffix": "k+", "label": "Live game servers tracked by a platform I ran for a decade", "short": "Game servers tracked live", "icon": "fa-solid fa-tower-broadcast" },
++ { "num": 10, "suffix": "+", "label": "Years running my own company, from the code to the taxes", "short": "Years running my company", "icon": "fa-solid fa-building-flag" }
],
"network": [
{ "key": "live", "name": "OpenVibe.Live", "url": "https://openvibe.live", "role": "Live streaming", "blurb": "RTMP, WHIP and WebRTC ingest, multi-destination restream, VODs, clips, chat, live captions and translation.", "stat": "529 routes · 179k LOC", "image": "/assets/img/sites/live.webp" },
ddiff --git a/src/data/theme-pdf-manifest.json b/src/data/theme-pdf-manifest.json
iindex dc3308c..a060a75 100644
---- a/src/data/theme-pdf-manifest.json
++++ b/src/data/theme-pdf-manifest.json
@@@ -1,15 +1,15 @@
{
-- "amber": "/Alex_Frison_Resume_Themed_amber_1c4cdbb2.pdf",
-- "blue": "/Alex_Frison_Resume_Sep_2026_1c4cdbb2.pdf",
-- "cyan": "/Alex_Frison_Resume_Themed_cyan_1c4cdbb2.pdf",
-- "default": "/Alex_Frison_Resume_Sep_2026_1c4cdbb2.pdf",
-- "green": "/Alex_Frison_Resume_Themed_green_1c4cdbb2.pdf",
-- "indigo": "/Alex_Frison_Resume_Themed_indigo_1c4cdbb2.pdf",
-- "lime": "/Alex_Frison_Resume_Themed_lime_1c4cdbb2.pdf",
-- "orange": "/Alex_Frison_Resume_Themed_orange_1c4cdbb2.pdf",
-- "pink": "/Alex_Frison_Resume_Themed_pink_1c4cdbb2.pdf",
-- "red": "/Alex_Frison_Resume_Themed_red_1c4cdbb2.pdf",
-- "teal": "/Alex_Frison_Resume_Themed_teal_1c4cdbb2.pdf",
-- "violet": "/Alex_Frison_Resume_Themed_violet_1c4cdbb2.pdf",
-- "yellow": "/Alex_Frison_Resume_Themed_yellow_1c4cdbb2.pdf"
++ "amber": "/Alex_Frison_Resume_Themed_amber_76413598.pdf",
++ "blue": "/Alex_Frison_Resume_Sep_2026_76413598.pdf",
++ "cyan": "/Alex_Frison_Resume_Themed_cyan_76413598.pdf",
++ "default": "/Alex_Frison_Resume_Sep_2026_76413598.pdf",
++ "green": "/Alex_Frison_Resume_Themed_green_76413598.pdf",
++ "indigo": "/Alex_Frison_Resume_Themed_indigo_76413598.pdf",
++ "lime": "/Alex_Frison_Resume_Themed_lime_76413598.pdf",
++ "orange": "/Alex_Frison_Resume_Themed_orange_76413598.pdf",
++ "pink": "/Alex_Frison_Resume_Themed_pink_76413598.pdf",
++ "red": "/Alex_Frison_Resume_Themed_red_76413598.pdf",
++ "teal": "/Alex_Frison_Resume_Themed_teal_76413598.pdf",
++ "violet": "/Alex_Frison_Resume_Themed_violet_76413598.pdf",
++ "yellow": "/Alex_Frison_Resume_Themed_yellow_76413598.pdf"
}