[READ-ONLY] Mirror of https://github.com/jmrplens/jmrplens. Profile jmrp.io/
0

Configure Feed

Select the types of activity you want to include in your feed.

redesign: paneles integrados con el tema nativo de GitHub (Primer, claro+oscuro)

- Tokens exactos de Primer (bg/border/fg/muted/accent) extraídos de github.com
en claro y oscuro → las cajas parecen UI nativa de GitHub.
- Tipografía de instrumento: métricas/%/fecha en monoespaciada (stack mono de GH).
- Blog: fuera el morado y los 01/02/03; eyebrow de fecha + 'LATEST', título en
color de acento (lee como enlace).
- Stats: caja compacta con cabecera, oscilograma sutil (acústica/DSP), barra de
lenguajes segmentada + leyenda en 2 columnas.
- Badges restantes al azul de GitHub (0969da).

Claude-Session: https://claude.ai/code/session_01AmXwVVm6y54gtHPxUqRpn1

José M. Requena Plens (Jun 22, 2026, 5:30 PM +0200) 474e63c2 99374415

+292 -267
+3 -3
README.md
··· 59 59 60 60 <p align="center"> 61 61 <a href="https://jmrp.io/blog"> 62 - <img src="https://img.shields.io/badge/📚_Read_all_posts-667eea?style=for-the-badge" alt="Read all posts"/> 62 + <img src="https://img.shields.io/badge/📚_Read_all_posts-0969da?style=for-the-badge" alt="Read all posts"/> 63 63 </a> 64 64 </p> 65 65 <!-- BLOG-POSTS:END --> ··· 73 73 74 74 <p align="center"> 75 75 <a href="https://jmrp.io"> 76 - <img src="https://img.shields.io/badge/-Website-667eea?style=flat-square&logo=googlechrome&logoColor=white" alt="Website"/> 76 + <img src="https://img.shields.io/badge/-Website-0969da?style=flat-square&logo=googlechrome&logoColor=white" alt="Website"/> 77 77 </a> 78 78 <a href="https://www.linkedin.com/in/jmrplens/"> 79 79 <img src="https://img.shields.io/badge/-LinkedIn-0A66C2?style=flat-square&logo=linkedin&logoColor=white" alt="LinkedIn"/> ··· 84 84 </p> 85 85 86 86 <p align="center"> 87 - <img src="https://komarev.com/ghpvc/?username=jmrplens&style=flat-square&color=667eea" alt="Profile Views"/> 87 + <img src="https://komarev.com/ghpvc/?username=jmrplens&style=flat-square&color=0969da" alt="Profile Views"/> 88 88 </p>
+14 -22
generated/blog-1-dark.svg
··· 1 - <svg width="800" height="176" viewBox="0 0 800 176" xmlns="http://www.w3.org/2000/svg"> 2 - <defs> 3 - <linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%"> 4 - <stop offset="0%" style="stop-color:#667eea"/> 5 - <stop offset="100%" style="stop-color:#764ba2"/> 6 - </linearGradient> 7 - <style> 8 - .card-index { font: 700 28px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; } 9 - .card-date { font: 600 11px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; opacity: 0.85; } 10 - .card-title { font: 700 20px 'Segoe UI', Ubuntu, sans-serif; fill: #c9d1d9; } 11 - .card-desc { font: 400 14px 'Segoe UI', Ubuntu, sans-serif; fill: #8b949e; } 12 - .card-more { font: 600 14px 'Segoe UI', Ubuntu, sans-serif; fill: #667eea; } 13 - </style> 14 - </defs> 15 - <rect x="0.5" y="0.5" width="799" height="175" rx="12" fill="#0d1117" stroke="#30363d" stroke-width="1"/> 16 - <rect x="0" y="0" width="58" height="176" rx="12" fill="url(#grad1)"/> 17 - <rect x="46" y="0" width="12" height="176" fill="url(#grad1)"/> 18 - <text class="card-index" x="29" y="68" text-anchor="middle">01</text> 19 - <text class="card-date" x="29" y="88" text-anchor="middle">Jun 2026</text> 20 - <text class="card-title" x="80" y="48">Your 4-Digit PIN Is Fine: Device-Bound Keys on</text><text class="card-title" x="80" y="75">ESP32-S3</text> 21 - <text class="card-desc" x="80" y="114">Why PBKDF2 iterations can't protect a 4-digit PIN on a</text><text class="card-desc" x="80" y="135">microcontroller, and how an eFuse-HMAC device secret plus HKDF…</text> 22 - <text class="card-more" x="80" y="156">Read more →</text> 1 + <svg width="800" height="184" viewBox="0 0 800 184" xmlns="http://www.w3.org/2000/svg"> 2 + <style> 3 + .b-date { font: 500 11px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #9198a1; letter-spacing: 0.5px; } 4 + .b-latest { font: 600 10px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #4493f8; letter-spacing: 0.5px; } 5 + .b-title { font: 600 18px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #4493f8; } 6 + .b-desc { font: 400 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #9198a1; } 7 + .b-more { font: 500 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #4493f8; } 8 + </style> 9 + <rect x="0.5" y="0.5" width="799" height="183" rx="6" fill="#0d1117" stroke="#3d444d"/> 10 + <text class="b-date" x="22" y="30">JUN 2026</text> 11 + <rect x="716" y="18" width="62" height="17" rx="8.5" fill="none" stroke="#4493f8"/><text class="b-latest" x="747" y="30" text-anchor="middle">LATEST</text> 12 + <text class="b-title" x="22" y="56">Your 4-Digit PIN Is Fine: Device-Bound Keys on</text><text class="b-title" x="22" y="81">ESP32-S3</text> 13 + <text class="b-desc" x="22" y="118">Why PBKDF2 iterations can't protect a 4-digit PIN on a microcontroller, and</text><text class="b-desc" x="22" y="138">how an eFuse-HMAC device secret plus HKDF stops offline brute force.</text> 14 + <text class="b-more" x="22" y="168">Read more →</text> 23 15 </svg>
+14 -22
generated/blog-1-light.svg
··· 1 - <svg width="800" height="176" viewBox="0 0 800 176" xmlns="http://www.w3.org/2000/svg"> 2 - <defs> 3 - <linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%"> 4 - <stop offset="0%" style="stop-color:#667eea"/> 5 - <stop offset="100%" style="stop-color:#764ba2"/> 6 - </linearGradient> 7 - <style> 8 - .card-index { font: 700 28px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; } 9 - .card-date { font: 600 11px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; opacity: 0.85; } 10 - .card-title { font: 700 20px 'Segoe UI', Ubuntu, sans-serif; fill: #24292e; } 11 - .card-desc { font: 400 14px 'Segoe UI', Ubuntu, sans-serif; fill: #586069; } 12 - .card-more { font: 600 14px 'Segoe UI', Ubuntu, sans-serif; fill: #667eea; } 13 - </style> 14 - </defs> 15 - <rect x="0.5" y="0.5" width="799" height="175" rx="12" fill="#ffffff" stroke="#e1e4e8" stroke-width="1"/> 16 - <rect x="0" y="0" width="58" height="176" rx="12" fill="url(#grad1)"/> 17 - <rect x="46" y="0" width="12" height="176" fill="url(#grad1)"/> 18 - <text class="card-index" x="29" y="68" text-anchor="middle">01</text> 19 - <text class="card-date" x="29" y="88" text-anchor="middle">Jun 2026</text> 20 - <text class="card-title" x="80" y="48">Your 4-Digit PIN Is Fine: Device-Bound Keys on</text><text class="card-title" x="80" y="75">ESP32-S3</text> 21 - <text class="card-desc" x="80" y="114">Why PBKDF2 iterations can't protect a 4-digit PIN on a</text><text class="card-desc" x="80" y="135">microcontroller, and how an eFuse-HMAC device secret plus HKDF…</text> 22 - <text class="card-more" x="80" y="156">Read more →</text> 1 + <svg width="800" height="184" viewBox="0 0 800 184" xmlns="http://www.w3.org/2000/svg"> 2 + <style> 3 + .b-date { font: 500 11px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #59636e; letter-spacing: 0.5px; } 4 + .b-latest { font: 600 10px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #0969da; letter-spacing: 0.5px; } 5 + .b-title { font: 600 18px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #0969da; } 6 + .b-desc { font: 400 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #59636e; } 7 + .b-more { font: 500 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #0969da; } 8 + </style> 9 + <rect x="0.5" y="0.5" width="799" height="183" rx="6" fill="#ffffff" stroke="#d1d9e0"/> 10 + <text class="b-date" x="22" y="30">JUN 2026</text> 11 + <rect x="716" y="18" width="62" height="17" rx="8.5" fill="none" stroke="#0969da"/><text class="b-latest" x="747" y="30" text-anchor="middle">LATEST</text> 12 + <text class="b-title" x="22" y="56">Your 4-Digit PIN Is Fine: Device-Bound Keys on</text><text class="b-title" x="22" y="81">ESP32-S3</text> 13 + <text class="b-desc" x="22" y="118">Why PBKDF2 iterations can't protect a 4-digit PIN on a microcontroller, and</text><text class="b-desc" x="22" y="138">how an eFuse-HMAC device secret plus HKDF stops offline brute force.</text> 14 + <text class="b-more" x="22" y="168">Read more →</text> 23 15 </svg>
+14 -22
generated/blog-2-dark.svg
··· 1 - <svg width="800" height="176" viewBox="0 0 800 176" xmlns="http://www.w3.org/2000/svg"> 2 - <defs> 3 - <linearGradient id="grad2" x1="0%" y1="0%" x2="0%" y2="100%"> 4 - <stop offset="0%" style="stop-color:#667eea"/> 5 - <stop offset="100%" style="stop-color:#764ba2"/> 6 - </linearGradient> 7 - <style> 8 - .card-index { font: 700 28px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; } 9 - .card-date { font: 600 11px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; opacity: 0.85; } 10 - .card-title { font: 700 20px 'Segoe UI', Ubuntu, sans-serif; fill: #c9d1d9; } 11 - .card-desc { font: 400 14px 'Segoe UI', Ubuntu, sans-serif; fill: #8b949e; } 12 - .card-more { font: 600 14px 'Segoe UI', Ubuntu, sans-serif; fill: #667eea; } 13 - </style> 14 - </defs> 15 - <rect x="0.5" y="0.5" width="799" height="175" rx="12" fill="#0d1117" stroke="#30363d" stroke-width="1"/> 16 - <rect x="0" y="0" width="58" height="176" rx="12" fill="url(#grad2)"/> 17 - <rect x="46" y="0" width="12" height="176" fill="url(#grad2)"/> 18 - <text class="card-index" x="29" y="68" text-anchor="middle">02</text> 19 - <text class="card-date" x="29" y="88" text-anchor="middle">Jun 2026</text> 20 - <text class="card-title" x="80" y="48">A Vault File That Fails Closed:</text><text class="card-title" x="80" y="75">Encrypt-then-MAC on an MCU</text> 21 - <text class="card-desc" x="80" y="114">How a hardware password manager authenticates every vault file</text><text class="card-desc" x="80" y="135">before it decrypts: encrypt-then-MAC, verify-before-decrypt, an…</text> 22 - <text class="card-more" x="80" y="156">Read more →</text> 1 + <svg width="800" height="184" viewBox="0 0 800 184" xmlns="http://www.w3.org/2000/svg"> 2 + <style> 3 + .b-date { font: 500 11px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #9198a1; letter-spacing: 0.5px; } 4 + .b-latest { font: 600 10px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #4493f8; letter-spacing: 0.5px; } 5 + .b-title { font: 600 18px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #4493f8; } 6 + .b-desc { font: 400 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #9198a1; } 7 + .b-more { font: 500 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #4493f8; } 8 + </style> 9 + <rect x="0.5" y="0.5" width="799" height="183" rx="6" fill="#0d1117" stroke="#3d444d"/> 10 + <text class="b-date" x="22" y="30">JUN 2026</text> 11 + 12 + <text class="b-title" x="22" y="56">A Vault File That Fails Closed: Encrypt-then-MAC on</text><text class="b-title" x="22" y="81">an MCU</text> 13 + <text class="b-desc" x="22" y="118">How a hardware password manager authenticates every vault file before it</text><text class="b-desc" x="22" y="138">decrypts: encrypt-then-MAC, verify-before-decrypt, and fail-closed reads.</text> 14 + <text class="b-more" x="22" y="168">Read more →</text> 23 15 </svg>
+14 -22
generated/blog-2-light.svg
··· 1 - <svg width="800" height="176" viewBox="0 0 800 176" xmlns="http://www.w3.org/2000/svg"> 2 - <defs> 3 - <linearGradient id="grad2" x1="0%" y1="0%" x2="0%" y2="100%"> 4 - <stop offset="0%" style="stop-color:#667eea"/> 5 - <stop offset="100%" style="stop-color:#764ba2"/> 6 - </linearGradient> 7 - <style> 8 - .card-index { font: 700 28px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; } 9 - .card-date { font: 600 11px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; opacity: 0.85; } 10 - .card-title { font: 700 20px 'Segoe UI', Ubuntu, sans-serif; fill: #24292e; } 11 - .card-desc { font: 400 14px 'Segoe UI', Ubuntu, sans-serif; fill: #586069; } 12 - .card-more { font: 600 14px 'Segoe UI', Ubuntu, sans-serif; fill: #667eea; } 13 - </style> 14 - </defs> 15 - <rect x="0.5" y="0.5" width="799" height="175" rx="12" fill="#ffffff" stroke="#e1e4e8" stroke-width="1"/> 16 - <rect x="0" y="0" width="58" height="176" rx="12" fill="url(#grad2)"/> 17 - <rect x="46" y="0" width="12" height="176" fill="url(#grad2)"/> 18 - <text class="card-index" x="29" y="68" text-anchor="middle">02</text> 19 - <text class="card-date" x="29" y="88" text-anchor="middle">Jun 2026</text> 20 - <text class="card-title" x="80" y="48">A Vault File That Fails Closed:</text><text class="card-title" x="80" y="75">Encrypt-then-MAC on an MCU</text> 21 - <text class="card-desc" x="80" y="114">How a hardware password manager authenticates every vault file</text><text class="card-desc" x="80" y="135">before it decrypts: encrypt-then-MAC, verify-before-decrypt, an…</text> 22 - <text class="card-more" x="80" y="156">Read more →</text> 1 + <svg width="800" height="184" viewBox="0 0 800 184" xmlns="http://www.w3.org/2000/svg"> 2 + <style> 3 + .b-date { font: 500 11px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #59636e; letter-spacing: 0.5px; } 4 + .b-latest { font: 600 10px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #0969da; letter-spacing: 0.5px; } 5 + .b-title { font: 600 18px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #0969da; } 6 + .b-desc { font: 400 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #59636e; } 7 + .b-more { font: 500 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #0969da; } 8 + </style> 9 + <rect x="0.5" y="0.5" width="799" height="183" rx="6" fill="#ffffff" stroke="#d1d9e0"/> 10 + <text class="b-date" x="22" y="30">JUN 2026</text> 11 + 12 + <text class="b-title" x="22" y="56">A Vault File That Fails Closed: Encrypt-then-MAC on</text><text class="b-title" x="22" y="81">an MCU</text> 13 + <text class="b-desc" x="22" y="118">How a hardware password manager authenticates every vault file before it</text><text class="b-desc" x="22" y="138">decrypts: encrypt-then-MAC, verify-before-decrypt, and fail-closed reads.</text> 14 + <text class="b-more" x="22" y="168">Read more →</text> 23 15 </svg>
+14 -22
generated/blog-3-dark.svg
··· 1 - <svg width="800" height="176" viewBox="0 0 800 176" xmlns="http://www.w3.org/2000/svg"> 2 - <defs> 3 - <linearGradient id="grad3" x1="0%" y1="0%" x2="0%" y2="100%"> 4 - <stop offset="0%" style="stop-color:#667eea"/> 5 - <stop offset="100%" style="stop-color:#764ba2"/> 6 - </linearGradient> 7 - <style> 8 - .card-index { font: 700 28px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; } 9 - .card-date { font: 600 11px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; opacity: 0.85; } 10 - .card-title { font: 700 20px 'Segoe UI', Ubuntu, sans-serif; fill: #c9d1d9; } 11 - .card-desc { font: 400 14px 'Segoe UI', Ubuntu, sans-serif; fill: #8b949e; } 12 - .card-more { font: 600 14px 'Segoe UI', Ubuntu, sans-serif; fill: #667eea; } 13 - </style> 14 - </defs> 15 - <rect x="0.5" y="0.5" width="799" height="175" rx="12" fill="#0d1117" stroke="#30363d" stroke-width="1"/> 16 - <rect x="0" y="0" width="58" height="176" rx="12" fill="url(#grad3)"/> 17 - <rect x="46" y="0" width="12" height="176" fill="url(#grad3)"/> 18 - <text class="card-index" x="29" y="68" text-anchor="middle">03</text> 19 - <text class="card-date" x="29" y="88" text-anchor="middle">Jun 2026</text> 20 - <text class="card-title" x="80" y="48">What the Linker Won't Do: Packing i18n Strings</text><text class="card-title" x="80" y="75">on an MCU</text> 21 - <text class="card-desc" x="80" y="114">A build-time generator packs firmware UI translations into one</text><text class="card-desc" x="80" y="135">string pool indexed by uint16 offsets, halving the index table…</text> 22 - <text class="card-more" x="80" y="156">Read more →</text> 1 + <svg width="800" height="184" viewBox="0 0 800 184" xmlns="http://www.w3.org/2000/svg"> 2 + <style> 3 + .b-date { font: 500 11px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #9198a1; letter-spacing: 0.5px; } 4 + .b-latest { font: 600 10px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #4493f8; letter-spacing: 0.5px; } 5 + .b-title { font: 600 18px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #4493f8; } 6 + .b-desc { font: 400 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #9198a1; } 7 + .b-more { font: 500 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #4493f8; } 8 + </style> 9 + <rect x="0.5" y="0.5" width="799" height="183" rx="6" fill="#0d1117" stroke="#3d444d"/> 10 + <text class="b-date" x="22" y="30">JUN 2026</text> 11 + 12 + <text class="b-title" x="22" y="56">What the Linker Won't Do: Packing i18n Strings on an</text><text class="b-title" x="22" y="81">MCU</text> 13 + <text class="b-desc" x="22" y="118">A build-time generator packs firmware UI translations into one string pool</text><text class="b-desc" x="22" y="138">indexed by uint16 offsets, halving the index table on a 32-bit MCU.</text> 14 + <text class="b-more" x="22" y="168">Read more →</text> 23 15 </svg>
+14 -22
generated/blog-3-light.svg
··· 1 - <svg width="800" height="176" viewBox="0 0 800 176" xmlns="http://www.w3.org/2000/svg"> 2 - <defs> 3 - <linearGradient id="grad3" x1="0%" y1="0%" x2="0%" y2="100%"> 4 - <stop offset="0%" style="stop-color:#667eea"/> 5 - <stop offset="100%" style="stop-color:#764ba2"/> 6 - </linearGradient> 7 - <style> 8 - .card-index { font: 700 28px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; } 9 - .card-date { font: 600 11px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; opacity: 0.85; } 10 - .card-title { font: 700 20px 'Segoe UI', Ubuntu, sans-serif; fill: #24292e; } 11 - .card-desc { font: 400 14px 'Segoe UI', Ubuntu, sans-serif; fill: #586069; } 12 - .card-more { font: 600 14px 'Segoe UI', Ubuntu, sans-serif; fill: #667eea; } 13 - </style> 14 - </defs> 15 - <rect x="0.5" y="0.5" width="799" height="175" rx="12" fill="#ffffff" stroke="#e1e4e8" stroke-width="1"/> 16 - <rect x="0" y="0" width="58" height="176" rx="12" fill="url(#grad3)"/> 17 - <rect x="46" y="0" width="12" height="176" fill="url(#grad3)"/> 18 - <text class="card-index" x="29" y="68" text-anchor="middle">03</text> 19 - <text class="card-date" x="29" y="88" text-anchor="middle">Jun 2026</text> 20 - <text class="card-title" x="80" y="48">What the Linker Won't Do: Packing i18n Strings</text><text class="card-title" x="80" y="75">on an MCU</text> 21 - <text class="card-desc" x="80" y="114">A build-time generator packs firmware UI translations into one</text><text class="card-desc" x="80" y="135">string pool indexed by uint16 offsets, halving the index table…</text> 22 - <text class="card-more" x="80" y="156">Read more →</text> 1 + <svg width="800" height="184" viewBox="0 0 800 184" xmlns="http://www.w3.org/2000/svg"> 2 + <style> 3 + .b-date { font: 500 11px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #59636e; letter-spacing: 0.5px; } 4 + .b-latest { font: 600 10px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #0969da; letter-spacing: 0.5px; } 5 + .b-title { font: 600 18px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #0969da; } 6 + .b-desc { font: 400 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #59636e; } 7 + .b-more { font: 500 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #0969da; } 8 + </style> 9 + <rect x="0.5" y="0.5" width="799" height="183" rx="6" fill="#ffffff" stroke="#d1d9e0"/> 10 + <text class="b-date" x="22" y="30">JUN 2026</text> 11 + 12 + <text class="b-title" x="22" y="56">What the Linker Won't Do: Packing i18n Strings on an</text><text class="b-title" x="22" y="81">MCU</text> 13 + <text class="b-desc" x="22" y="118">A build-time generator packs firmware UI translations into one string pool</text><text class="b-desc" x="22" y="138">indexed by uint16 offsets, halving the index table on a 32-bit MCU.</text> 14 + <text class="b-more" x="22" y="168">Read more →</text> 23 15 </svg>
+24 -24
generated/github-stats-dark.svg
··· 1 - <svg width="800" height="527" viewBox="0 0 800 527" xmlns="http://www.w3.org/2000/svg"> 1 + <svg width="800" height="336" viewBox="0 0 800 336" xmlns="http://www.w3.org/2000/svg"> 2 2 <defs> 3 - <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%"> 4 - <stop offset="0%" style="stop-color:#667eea"/> 5 - <stop offset="100%" style="stop-color:#764ba2"/> 6 - </linearGradient> 7 - <style> 8 - .title { font: 600 18px 'Segoe UI', Ubuntu, sans-serif; fill: #c9d1d9; } 9 - .stat-label { font: 400 12px 'Segoe UI', Ubuntu, sans-serif; fill: #8b949e; } 10 - .stat-value { font: 700 22px 'Segoe UI', Ubuntu, sans-serif; fill: #c9d1d9; } 11 - .lang-name { font: 400 13px 'Segoe UI', Ubuntu, sans-serif; fill: #8b949e; } 12 - .lang-pct { font: 600 12px 'Segoe UI', Ubuntu, sans-serif; fill: #c9d1d9; } 13 - .footer { font: 400 11px 'Segoe UI', Ubuntu, sans-serif; fill: #8b949e; } 14 - </style> 3 + <clipPath id="card"><rect x="0.5" y="0.5" width="799" height="335" rx="6"/></clipPath> 15 4 </defs> 16 - <rect width="800" height="527" rx="10" fill="#0d1117" stroke="#30363d" stroke-width="1"/> 17 - <rect x="0" y="0" width="6" height="527" rx="3" fill="url(#grad)"/> 18 - <text class="title" x="25" y="38">GitHub Statistics</text> 19 - <g transform="translate(25, 70)"><g transform="translate(0, 0)"><text class="stat-label" x="0" y="0">Repositories</text><text class="stat-value" x="0" y="28">23</text></g><g transform="translate(152, 0)"><text class="stat-label" x="0" y="0">Stars</text><text class="stat-value" x="0" y="28">304</text></g><g transform="translate(304, 0)"><text class="stat-label" x="0" y="0">Followers</text><text class="stat-value" x="0" y="28">34</text></g><g transform="translate(456, 0)"><text class="stat-label" x="0" y="0">Commits (yr)</text><text class="stat-value" x="0" y="28">455</text></g><g transform="translate(608, 0)"><text class="stat-label" x="0" y="0">Contributions (yr)</text><text class="stat-value" x="0" y="28">1,520</text></g></g> 20 - <line x1="25" y1="120" x2="775" y2="120" stroke="#30363d" stroke-width="1"/> 21 - <text class="title" x="25" y="160">Most Used Languages</text> 22 - <text class="stat-label" x="25" y="180">Normalized share across repositories (public + private)</text> 23 - <g transform="translate(25, 215)"> 24 - <g transform="translate(0, 0)"><circle cx="7" cy="9" r="6.5" fill="#e16737"/><text class="lang-name" x="22" y="14">MATLAB</text><rect x="130" y="3" width="151.8" height="12" rx="6" fill="#e16737" opacity="0.85"/><text class="lang-pct" x="289.8" y="14">32%</text></g><g transform="translate(0, 34)"><circle cx="7" cy="9" r="6.5" fill="#3572A5"/><text class="lang-name" x="22" y="14">Python</text><rect x="130" y="3" width="84.5" height="12" rx="6" fill="#3572A5" opacity="0.85"/><text class="lang-pct" x="222.5" y="14">18%</text></g><g transform="translate(0, 68)"><circle cx="7" cy="9" r="6.5" fill="#00ADD8"/><text class="lang-name" x="22" y="14">Go</text><rect x="130" y="3" width="58.5" height="12" rx="6" fill="#00ADD8" opacity="0.85"/><text class="lang-pct" x="196.5" y="14">12%</text></g><g transform="translate(0, 102)"><circle cx="7" cy="9" r="6.5" fill="#3D6117"/><text class="lang-name" x="22" y="14">TeX</text><rect x="130" y="3" width="49.7" height="12" rx="6" fill="#3D6117" opacity="0.85"/><text class="lang-pct" x="187.7" y="14">10%</text></g><g transform="translate(0, 136)"><circle cx="7" cy="9" r="6.5" fill="#555555"/><text class="lang-name" x="22" y="14">C</text><rect x="130" y="3" width="45.0" height="12" rx="6" fill="#555555" opacity="0.85"/><text class="lang-pct" x="183.0" y="14">9%</text></g><g transform="translate(0, 170)"><circle cx="7" cy="9" r="6.5" fill="#89e051"/><text class="lang-name" x="22" y="14">Shell</text><rect x="130" y="3" width="35.0" height="12" rx="6" fill="#89e051" opacity="0.85"/><text class="lang-pct" x="173.0" y="14">7%</text></g><g transform="translate(0, 204)"><circle cx="7" cy="9" r="6.5" fill="#f34b7d"/><text class="lang-name" x="22" y="14">C++</text><rect x="130" y="3" width="34.8" height="12" rx="6" fill="#f34b7d" opacity="0.85"/><text class="lang-pct" x="172.8" y="14">7%</text></g><g transform="translate(0, 238)"><circle cx="7" cy="9" r="6.5" fill="#e34c26"/><text class="lang-name" x="22" y="14">HTML</text><rect x="130" y="3" width="20.8" height="12" rx="6" fill="#e34c26" opacity="0.85"/><text class="lang-pct" x="158.8" y="14">4%</text></g> 25 - </g> 26 - <text class="footer" x="400" y="513" text-anchor="middle">Updated 2026-06-22 14:43 UTC</text> 5 + <style> 6 + .h-title { font: 600 15px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #f0f6fc; } 7 + .s-title { font: 600 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #f0f6fc; } 8 + .s-sub { font: 400 11px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #9198a1; } 9 + .m-val { font: 600 22px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #f0f6fc; } 10 + .m-lbl { font: 400 11px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #9198a1; } 11 + .lg-name { font: 400 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #f0f6fc; } 12 + .lg-pct { font: 500 13px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #9198a1; } 13 + .foot { font: 400 10px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #9198a1; } 14 + </style> 15 + <rect x="0.5" y="0.5" width="799" height="335" rx="6" fill="#0d1117"/> 16 + <g clip-path="url(#card)"><rect x="0" y="0" width="800" height="50" fill="#151b23"/></g> 17 + <line x1="0.5" y1="50" x2="799.5" y2="50" stroke="#3d444d"/> 18 + <rect x="0.5" y="0.5" width="799" height="335" rx="6" fill="none" stroke="#3d444d"/> 19 + <path d="M568.0,27.0 L570.9,29.4 L573.8,31.5 L576.8,33.1 L579.7,33.9 L582.6,33.9 L585.5,33.1 L588.4,31.5 L591.3,29.4 L594.2,27.0 L597.2,24.6 L600.1,22.5 L603.0,20.9 L605.9,20.1 L608.8,20.1 L611.8,20.9 L614.7,22.5 L617.6,24.6 L620.5,27.0 L623.4,29.4 L626.3,31.5 L629.2,33.1 L632.2,33.9 L635.1,33.9 L638.0,33.1 L640.9,31.5 L643.8,29.4 L646.8,27.0 L649.7,24.6 L652.6,22.5 L655.5,20.9 L658.4,20.1 L661.3,20.1 L664.2,20.9 L667.2,22.5 L670.1,24.6 L673.0,27.0 L675.9,29.4 L678.8,31.5 L681.8,33.1 L684.7,33.9 L687.6,33.9 L690.5,33.1 L693.4,31.5 L696.3,29.4 L699.2,27.0 L702.2,24.6 L705.1,22.5 L708.0,20.9 L710.9,20.1 L713.8,20.1 L716.8,20.9 L719.7,22.5 L722.6,24.6 L725.5,27.0 L728.4,29.4 L731.3,31.5 L734.2,33.1 L737.2,33.9 L740.1,33.9 L743.0,33.1 L745.9,31.5 L748.8,29.4 L751.8,27.0 L754.7,24.6 L757.6,22.5 L760.5,20.9 L763.4,20.1 L766.3,20.1 L769.2,20.9 L772.2,22.5 L775.1,24.6 L778.0,27.0" fill="none" stroke="#4493f8" stroke-width="1.5" opacity="0.45"/> 20 + <text class="h-title" x="22" y="31">GitHub Statistics</text> 21 + <g transform="translate(22, 88)"><g transform="translate(0, 0)"><text class="m-val" x="0" y="0">23</text><text class="m-lbl" x="0" y="18">Repositories</text></g><g transform="translate(151, 0)"><text class="m-val" x="0" y="0">304</text><text class="m-lbl" x="0" y="18">Stars</text></g><g transform="translate(302, 0)"><text class="m-val" x="0" y="0">34</text><text class="m-lbl" x="0" y="18">Followers</text></g><g transform="translate(454, 0)"><text class="m-val" x="0" y="0">1,136</text><text class="m-lbl" x="0" y="18">Commits / yr</text></g><g transform="translate(605, 0)"><text class="m-val" x="0" y="0">1,536</text><text class="m-lbl" x="0" y="18">Contributions / yr</text></g></g> 22 + <text class="s-title" x="22" y="140">Most Used Languages</text> 23 + <text class="s-sub" x="22" y="157">Normalized share across repositories · public + private</text> 24 + <g transform="translate(22, 174)"><rect x="0.0" y="0" width="237.1" height="14" rx="3" fill="#e16737"/><rect x="239.1" y="0" width="131.1" height="14" rx="3" fill="#3572A5"/><rect x="372.2" y="0" width="90.2" height="14" rx="3" fill="#00ADD8"/><rect x="464.4" y="0" width="76.2" height="14" rx="3" fill="#3D6117"/><rect x="542.6" y="0" width="68.9" height="14" rx="3" fill="#555555"/><rect x="613.5" y="0" width="53.1" height="14" rx="3" fill="#89e051"/><rect x="668.5" y="0" width="52.8" height="14" rx="3" fill="#f34b7d"/><rect x="723.3" y="0" width="30.7" height="14" rx="3" fill="#e34c26"/></g> 25 + <g transform="translate(22, 214)"><g transform="translate(0, 0)"><circle cx="6" cy="-4" r="6" fill="#e16737"/><text class="lg-name" x="20" y="0">MATLAB</text><text class="lg-pct" x="358" y="0" text-anchor="end">32%</text></g><g transform="translate(0, 25)"><circle cx="6" cy="-4" r="6" fill="#3572A5"/><text class="lg-name" x="20" y="0">Python</text><text class="lg-pct" x="358" y="0" text-anchor="end">18%</text></g><g transform="translate(0, 50)"><circle cx="6" cy="-4" r="6" fill="#00ADD8"/><text class="lg-name" x="20" y="0">Go</text><text class="lg-pct" x="358" y="0" text-anchor="end">12%</text></g><g transform="translate(0, 75)"><circle cx="6" cy="-4" r="6" fill="#3D6117"/><text class="lg-name" x="20" y="0">TeX</text><text class="lg-pct" x="358" y="0" text-anchor="end">10%</text></g><g transform="translate(378, 0)"><circle cx="6" cy="-4" r="6" fill="#555555"/><text class="lg-name" x="20" y="0">C</text><text class="lg-pct" x="358" y="0" text-anchor="end">9%</text></g><g transform="translate(378, 25)"><circle cx="6" cy="-4" r="6" fill="#89e051"/><text class="lg-name" x="20" y="0">Shell</text><text class="lg-pct" x="358" y="0" text-anchor="end">7%</text></g><g transform="translate(378, 50)"><circle cx="6" cy="-4" r="6" fill="#f34b7d"/><text class="lg-name" x="20" y="0">C++</text><text class="lg-pct" x="358" y="0" text-anchor="end">7%</text></g><g transform="translate(378, 75)"><circle cx="6" cy="-4" r="6" fill="#e34c26"/><text class="lg-name" x="20" y="0">HTML</text><text class="lg-pct" x="358" y="0" text-anchor="end">4%</text></g></g> 26 + <text class="foot" x="400" y="325" text-anchor="middle">updated 2026-06-22 17:30 UTC</text> 27 27 </svg>
+24 -24
generated/github-stats-light.svg
··· 1 - <svg width="800" height="527" viewBox="0 0 800 527" xmlns="http://www.w3.org/2000/svg"> 1 + <svg width="800" height="336" viewBox="0 0 800 336" xmlns="http://www.w3.org/2000/svg"> 2 2 <defs> 3 - <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%"> 4 - <stop offset="0%" style="stop-color:#667eea"/> 5 - <stop offset="100%" style="stop-color:#764ba2"/> 6 - </linearGradient> 7 - <style> 8 - .title { font: 600 18px 'Segoe UI', Ubuntu, sans-serif; fill: #24292e; } 9 - .stat-label { font: 400 12px 'Segoe UI', Ubuntu, sans-serif; fill: #586069; } 10 - .stat-value { font: 700 22px 'Segoe UI', Ubuntu, sans-serif; fill: #24292e; } 11 - .lang-name { font: 400 13px 'Segoe UI', Ubuntu, sans-serif; fill: #586069; } 12 - .lang-pct { font: 600 12px 'Segoe UI', Ubuntu, sans-serif; fill: #24292e; } 13 - .footer { font: 400 11px 'Segoe UI', Ubuntu, sans-serif; fill: #586069; } 14 - </style> 3 + <clipPath id="card"><rect x="0.5" y="0.5" width="799" height="335" rx="6"/></clipPath> 15 4 </defs> 16 - <rect width="800" height="527" rx="10" fill="#ffffff" stroke="#e1e4e8" stroke-width="1"/> 17 - <rect x="0" y="0" width="6" height="527" rx="3" fill="url(#grad)"/> 18 - <text class="title" x="25" y="38">GitHub Statistics</text> 19 - <g transform="translate(25, 70)"><g transform="translate(0, 0)"><text class="stat-label" x="0" y="0">Repositories</text><text class="stat-value" x="0" y="28">23</text></g><g transform="translate(152, 0)"><text class="stat-label" x="0" y="0">Stars</text><text class="stat-value" x="0" y="28">304</text></g><g transform="translate(304, 0)"><text class="stat-label" x="0" y="0">Followers</text><text class="stat-value" x="0" y="28">34</text></g><g transform="translate(456, 0)"><text class="stat-label" x="0" y="0">Commits (yr)</text><text class="stat-value" x="0" y="28">455</text></g><g transform="translate(608, 0)"><text class="stat-label" x="0" y="0">Contributions (yr)</text><text class="stat-value" x="0" y="28">1,520</text></g></g> 20 - <line x1="25" y1="120" x2="775" y2="120" stroke="#e1e4e8" stroke-width="1"/> 21 - <text class="title" x="25" y="160">Most Used Languages</text> 22 - <text class="stat-label" x="25" y="180">Normalized share across repositories (public + private)</text> 23 - <g transform="translate(25, 215)"> 24 - <g transform="translate(0, 0)"><circle cx="7" cy="9" r="6.5" fill="#e16737"/><text class="lang-name" x="22" y="14">MATLAB</text><rect x="130" y="3" width="151.8" height="12" rx="6" fill="#e16737" opacity="0.85"/><text class="lang-pct" x="289.8" y="14">32%</text></g><g transform="translate(0, 34)"><circle cx="7" cy="9" r="6.5" fill="#3572A5"/><text class="lang-name" x="22" y="14">Python</text><rect x="130" y="3" width="84.5" height="12" rx="6" fill="#3572A5" opacity="0.85"/><text class="lang-pct" x="222.5" y="14">18%</text></g><g transform="translate(0, 68)"><circle cx="7" cy="9" r="6.5" fill="#00ADD8"/><text class="lang-name" x="22" y="14">Go</text><rect x="130" y="3" width="58.5" height="12" rx="6" fill="#00ADD8" opacity="0.85"/><text class="lang-pct" x="196.5" y="14">12%</text></g><g transform="translate(0, 102)"><circle cx="7" cy="9" r="6.5" fill="#3D6117"/><text class="lang-name" x="22" y="14">TeX</text><rect x="130" y="3" width="49.7" height="12" rx="6" fill="#3D6117" opacity="0.85"/><text class="lang-pct" x="187.7" y="14">10%</text></g><g transform="translate(0, 136)"><circle cx="7" cy="9" r="6.5" fill="#555555"/><text class="lang-name" x="22" y="14">C</text><rect x="130" y="3" width="45.0" height="12" rx="6" fill="#555555" opacity="0.85"/><text class="lang-pct" x="183.0" y="14">9%</text></g><g transform="translate(0, 170)"><circle cx="7" cy="9" r="6.5" fill="#89e051"/><text class="lang-name" x="22" y="14">Shell</text><rect x="130" y="3" width="35.0" height="12" rx="6" fill="#89e051" opacity="0.85"/><text class="lang-pct" x="173.0" y="14">7%</text></g><g transform="translate(0, 204)"><circle cx="7" cy="9" r="6.5" fill="#f34b7d"/><text class="lang-name" x="22" y="14">C++</text><rect x="130" y="3" width="34.8" height="12" rx="6" fill="#f34b7d" opacity="0.85"/><text class="lang-pct" x="172.8" y="14">7%</text></g><g transform="translate(0, 238)"><circle cx="7" cy="9" r="6.5" fill="#e34c26"/><text class="lang-name" x="22" y="14">HTML</text><rect x="130" y="3" width="20.8" height="12" rx="6" fill="#e34c26" opacity="0.85"/><text class="lang-pct" x="158.8" y="14">4%</text></g> 25 - </g> 26 - <text class="footer" x="400" y="513" text-anchor="middle">Updated 2026-06-22 14:43 UTC</text> 5 + <style> 6 + .h-title { font: 600 15px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #1f2328; } 7 + .s-title { font: 600 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #1f2328; } 8 + .s-sub { font: 400 11px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #59636e; } 9 + .m-val { font: 600 22px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #1f2328; } 10 + .m-lbl { font: 400 11px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #59636e; } 11 + .lg-name { font: 400 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; fill: #1f2328; } 12 + .lg-pct { font: 500 13px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #59636e; } 13 + .foot { font: 400 10px ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; fill: #59636e; } 14 + </style> 15 + <rect x="0.5" y="0.5" width="799" height="335" rx="6" fill="#ffffff"/> 16 + <g clip-path="url(#card)"><rect x="0" y="0" width="800" height="50" fill="#f6f8fa"/></g> 17 + <line x1="0.5" y1="50" x2="799.5" y2="50" stroke="#d1d9e0"/> 18 + <rect x="0.5" y="0.5" width="799" height="335" rx="6" fill="none" stroke="#d1d9e0"/> 19 + <path d="M568.0,27.0 L570.9,29.4 L573.8,31.5 L576.8,33.1 L579.7,33.9 L582.6,33.9 L585.5,33.1 L588.4,31.5 L591.3,29.4 L594.2,27.0 L597.2,24.6 L600.1,22.5 L603.0,20.9 L605.9,20.1 L608.8,20.1 L611.8,20.9 L614.7,22.5 L617.6,24.6 L620.5,27.0 L623.4,29.4 L626.3,31.5 L629.2,33.1 L632.2,33.9 L635.1,33.9 L638.0,33.1 L640.9,31.5 L643.8,29.4 L646.8,27.0 L649.7,24.6 L652.6,22.5 L655.5,20.9 L658.4,20.1 L661.3,20.1 L664.2,20.9 L667.2,22.5 L670.1,24.6 L673.0,27.0 L675.9,29.4 L678.8,31.5 L681.8,33.1 L684.7,33.9 L687.6,33.9 L690.5,33.1 L693.4,31.5 L696.3,29.4 L699.2,27.0 L702.2,24.6 L705.1,22.5 L708.0,20.9 L710.9,20.1 L713.8,20.1 L716.8,20.9 L719.7,22.5 L722.6,24.6 L725.5,27.0 L728.4,29.4 L731.3,31.5 L734.2,33.1 L737.2,33.9 L740.1,33.9 L743.0,33.1 L745.9,31.5 L748.8,29.4 L751.8,27.0 L754.7,24.6 L757.6,22.5 L760.5,20.9 L763.4,20.1 L766.3,20.1 L769.2,20.9 L772.2,22.5 L775.1,24.6 L778.0,27.0" fill="none" stroke="#0969da" stroke-width="1.5" opacity="0.45"/> 20 + <text class="h-title" x="22" y="31">GitHub Statistics</text> 21 + <g transform="translate(22, 88)"><g transform="translate(0, 0)"><text class="m-val" x="0" y="0">23</text><text class="m-lbl" x="0" y="18">Repositories</text></g><g transform="translate(151, 0)"><text class="m-val" x="0" y="0">304</text><text class="m-lbl" x="0" y="18">Stars</text></g><g transform="translate(302, 0)"><text class="m-val" x="0" y="0">34</text><text class="m-lbl" x="0" y="18">Followers</text></g><g transform="translate(454, 0)"><text class="m-val" x="0" y="0">1,136</text><text class="m-lbl" x="0" y="18">Commits / yr</text></g><g transform="translate(605, 0)"><text class="m-val" x="0" y="0">1,536</text><text class="m-lbl" x="0" y="18">Contributions / yr</text></g></g> 22 + <text class="s-title" x="22" y="140">Most Used Languages</text> 23 + <text class="s-sub" x="22" y="157">Normalized share across repositories · public + private</text> 24 + <g transform="translate(22, 174)"><rect x="0.0" y="0" width="237.1" height="14" rx="3" fill="#e16737"/><rect x="239.1" y="0" width="131.1" height="14" rx="3" fill="#3572A5"/><rect x="372.2" y="0" width="90.2" height="14" rx="3" fill="#00ADD8"/><rect x="464.4" y="0" width="76.2" height="14" rx="3" fill="#3D6117"/><rect x="542.6" y="0" width="68.9" height="14" rx="3" fill="#555555"/><rect x="613.5" y="0" width="53.1" height="14" rx="3" fill="#89e051"/><rect x="668.5" y="0" width="52.8" height="14" rx="3" fill="#f34b7d"/><rect x="723.3" y="0" width="30.7" height="14" rx="3" fill="#e34c26"/></g> 25 + <g transform="translate(22, 214)"><g transform="translate(0, 0)"><circle cx="6" cy="-4" r="6" fill="#e16737"/><text class="lg-name" x="20" y="0">MATLAB</text><text class="lg-pct" x="358" y="0" text-anchor="end">32%</text></g><g transform="translate(0, 25)"><circle cx="6" cy="-4" r="6" fill="#3572A5"/><text class="lg-name" x="20" y="0">Python</text><text class="lg-pct" x="358" y="0" text-anchor="end">18%</text></g><g transform="translate(0, 50)"><circle cx="6" cy="-4" r="6" fill="#00ADD8"/><text class="lg-name" x="20" y="0">Go</text><text class="lg-pct" x="358" y="0" text-anchor="end">12%</text></g><g transform="translate(0, 75)"><circle cx="6" cy="-4" r="6" fill="#3D6117"/><text class="lg-name" x="20" y="0">TeX</text><text class="lg-pct" x="358" y="0" text-anchor="end">10%</text></g><g transform="translate(378, 0)"><circle cx="6" cy="-4" r="6" fill="#555555"/><text class="lg-name" x="20" y="0">C</text><text class="lg-pct" x="358" y="0" text-anchor="end">9%</text></g><g transform="translate(378, 25)"><circle cx="6" cy="-4" r="6" fill="#89e051"/><text class="lg-name" x="20" y="0">Shell</text><text class="lg-pct" x="358" y="0" text-anchor="end">7%</text></g><g transform="translate(378, 50)"><circle cx="6" cy="-4" r="6" fill="#f34b7d"/><text class="lg-name" x="20" y="0">C++</text><text class="lg-pct" x="358" y="0" text-anchor="end">7%</text></g><g transform="translate(378, 75)"><circle cx="6" cy="-4" r="6" fill="#e34c26"/><text class="lg-name" x="20" y="0">HTML</text><text class="lg-pct" x="358" y="0" text-anchor="end">4%</text></g></g> 26 + <text class="foot" x="400" y="325" text-anchor="middle">updated 2026-06-22 17:30 UTC</text> 27 27 </svg>
+1 -1
statsgen/readme.py
··· 7 7 _READ_ALL_BADGE = ( 8 8 '<p align="center">\n' 9 9 ' <a href="https://jmrp.io/blog">\n' 10 - ' <img src="https://img.shields.io/badge/📚_Read_all_posts-667eea?style=for-the-badge" ' 10 + ' <img src="https://img.shields.io/badge/📚_Read_all_posts-0969da?style=for-the-badge" ' 11 11 'alt="Read all posts"/>\n' 12 12 " </a>\n" 13 13 "</p>"
+41 -29
statsgen/render_blog.py
··· 1 - """Render de una tarjeta de blog como SVG temático y clicable (vía wrapper <a> en el README).""" 1 + """Tarjeta de blog como una caja nativa de GitHub (Primer). 2 + 3 + Sin el número 01/02/03 (el orden solo es recencia, que ya indica la fecha): 4 + eyebrow con la fecha en monoespaciada + 'LATEST' en la más reciente, título 5 + en color de acento (lee como enlace), descripción en muted. La tarjeta entera 6 + es clicable vía el <a> que la envuelve en el README. 7 + """ 2 8 import re 3 9 4 10 from statsgen.transform import wrap_text 5 - from statsgen.theme import THEMES 11 + from statsgen.theme import THEMES, FONT_SANS, FONT_MONO 6 12 7 13 WIDTH = 800 8 - HEIGHT = 176 14 + PAD = 22 9 15 10 16 11 17 def _esc(text): ··· 21 27 22 28 def render_blog_card_svg(post, index, theme): 23 29 t = THEMES[theme] 24 - title_lines = wrap_text(_strip_html(post["title"]), max_chars=46, max_lines=2) 25 - desc_lines = wrap_text(_strip_html(post["description"]), max_chars=64, max_lines=2) 26 - date = _esc(post.get("date", "")) 30 + title_lines = wrap_text(_strip_html(post["title"]), max_chars=52, max_lines=2) 31 + desc_lines = wrap_text(_strip_html(post["description"]), max_chars=78, max_lines=2) 32 + date = _esc(post.get("date", "")).upper() 27 33 34 + eyebrow_y = 30 35 + title_y0 = 56 36 + title_step = 25 28 37 title_svg = "".join( 29 - f'<text class="card-title" x="80" y="{48 + i * 27}">{_esc(line)}</text>' 38 + f'<text class="b-title" x="{PAD}" y="{title_y0 + i * title_step}">{_esc(line)}</text>' 30 39 for i, line in enumerate(title_lines) 31 40 ) 32 - desc_top = 48 + len(title_lines) * 27 + 12 41 + desc_top = title_y0 + len(title_lines) * title_step + 12 33 42 desc_svg = "".join( 34 - f'<text class="card-desc" x="80" y="{desc_top + i * 21}">{_esc(line)}</text>' 43 + f'<text class="b-desc" x="{PAD}" y="{desc_top + i * 20}">{_esc(line)}</text>' 35 44 for i, line in enumerate(desc_lines) 36 45 ) 46 + more_y = desc_top + (len(desc_lines) - 1) * 20 + 30 47 + height = more_y + 16 37 48 38 - return f'''<svg width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}" xmlns="http://www.w3.org/2000/svg"> 39 - <defs> 40 - <linearGradient id="grad{index}" x1="0%" y1="0%" x2="0%" y2="100%"> 41 - <stop offset="0%" style="stop-color:#667eea"/> 42 - <stop offset="100%" style="stop-color:#764ba2"/> 43 - </linearGradient> 44 - <style> 45 - .card-index {{ font: 700 28px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; }} 46 - .card-date {{ font: 600 11px 'Segoe UI', Ubuntu, sans-serif; fill: #ffffff; opacity: 0.85; }} 47 - .card-title {{ font: 700 20px 'Segoe UI', Ubuntu, sans-serif; fill: {t['title']}; }} 48 - .card-desc {{ font: 400 14px 'Segoe UI', Ubuntu, sans-serif; fill: {t['label']}; }} 49 - .card-more {{ font: 600 14px 'Segoe UI', Ubuntu, sans-serif; fill: #667eea; }} 50 - </style> 51 - </defs> 52 - <rect x="0.5" y="0.5" width="{WIDTH - 1}" height="{HEIGHT - 1}" rx="12" fill="{t['bg']}" stroke="{t['bg_stroke']}" stroke-width="1"/> 53 - <rect x="0" y="0" width="58" height="{HEIGHT}" rx="12" fill="url(#grad{index})"/> 54 - <rect x="46" y="0" width="12" height="{HEIGHT}" fill="url(#grad{index})"/> 55 - <text class="card-index" x="29" y="68" text-anchor="middle">{index:02d}</text> 56 - <text class="card-date" x="29" y="88" text-anchor="middle">{date}</text> 49 + # 'LATEST' eyebrow-pill sólo en la entrada más reciente (index == 1). 50 + latest = "" 51 + if index == 1: 52 + latest = ( 53 + f'<rect x="{WIDTH - PAD - 62}" y="18" width="62" height="17" rx="8.5" ' 54 + f'fill="none" stroke="{t["accent"]}"/>' 55 + f'<text class="b-latest" x="{WIDTH - PAD - 31}" y="30" text-anchor="middle">LATEST</text>' 56 + ) 57 + 58 + return f'''<svg width="{WIDTH}" height="{height}" viewBox="0 0 {WIDTH} {height}" xmlns="http://www.w3.org/2000/svg"> 59 + <style> 60 + .b-date {{ font: 500 11px {FONT_MONO}; fill: {t['muted']}; letter-spacing: 0.5px; }} 61 + .b-latest {{ font: 600 10px {FONT_MONO}; fill: {t['accent']}; letter-spacing: 0.5px; }} 62 + .b-title {{ font: 600 18px {FONT_SANS}; fill: {t['accent']}; }} 63 + .b-desc {{ font: 400 13px {FONT_SANS}; fill: {t['muted']}; }} 64 + .b-more {{ font: 500 13px {FONT_SANS}; fill: {t['accent']}; }} 65 + </style> 66 + <rect x="0.5" y="0.5" width="{WIDTH - 1}" height="{height - 1}" rx="6" fill="{t['bg']}" stroke="{t['border']}"/> 67 + <text class="b-date" x="{PAD}" y="{eyebrow_y}">{date}</text> 68 + {latest} 57 69 {title_svg} 58 70 {desc_svg} 59 - <text class="card-more" x="80" y="{HEIGHT - 20}">Read more →</text> 71 + <text class="b-more" x="{PAD}" y="{more_y}">Read more →</text> 60 72 </svg>'''
+82 -43
statsgen/render_stats.py
··· 1 - """Render del panel de estadísticas como SVG temático.""" 1 + """Panel de estadísticas como una caja nativa de GitHub (Primer). 2 + 3 + Cabecera con título + un sutil oscilograma (guiño a acústica/DSP), fila de 4 + métricas reales con los números en monoespaciada (estética de readout de 5 + instrumento), y los lenguajes como barra segmentada full-width + leyenda en 6 + dos columnas. Sin números inventados. 7 + """ 8 + import math 2 9 from datetime import datetime 3 10 4 - from statsgen.theme import THEMES, LANG_COLORS 11 + from statsgen.theme import THEMES, LANG_COLORS, FONT_SANS, FONT_MONO 5 12 6 13 WIDTH = 800 14 + PAD = 22 15 + INNER = WIDTH - 2 * PAD 16 + 17 + 18 + def _waveform(x0, x1, y, amp, cycles, n=72): 19 + """Polyline sinusoidal decorativa (oscilograma sutil de la cabecera).""" 20 + pts = [] 21 + for i in range(n + 1): 22 + tt = i / n 23 + px = x0 + (x1 - x0) * tt 24 + py = y + amp * math.sin(tt * cycles * 2 * math.pi) 25 + pts.append(f"{px:.1f},{py:.1f}") 26 + return "M" + " L".join(pts) 7 27 8 28 9 29 def _metric(x, label, value): 10 30 return ( 11 - f'<text class="stat-label" x="{x}" y="0">{label}</text>' 12 - f'<text class="stat-value" x="{x}" y="28">{value}</text>' 31 + f'<text class="m-val" x="{x}" y="0">{value}</text>' 32 + f'<text class="m-lbl" x="{x}" y="18">{label}</text>' 13 33 ) 14 34 15 35 16 36 def render_stats_svg(lang_pcts, activity, theme): 17 37 t = THEMES[theme] 18 - rows = len(lang_pcts) 19 - langs_top = 215 20 - height = langs_top + rows * 34 + 40 21 38 39 + header_h = 50 22 40 metrics = [ 23 41 ("Repositories", f"{activity['public_repos']:,}"), 24 42 ("Stars", f"{activity['total_stars']:,}"), 25 43 ("Followers", f"{activity['followers']:,}"), 26 - ("Commits (yr)", f"{activity['commits_year']:,}"), 27 - ("Contributions (yr)", f"{activity['contributions_year']:,}"), 44 + ("Commits / yr", f"{activity['commits_year']:,}"), 45 + ("Contributions / yr", f"{activity['contributions_year']:,}"), 28 46 ] 47 + mcol = INNER / len(metrics) 48 + metrics_y = header_h + 38 29 49 metric_svg = "".join( 30 - f'<g transform="translate({i * 152}, 0)">{_metric(0, lbl, val)}</g>' 50 + f'<g transform="translate({i * mcol:.0f}, 0)">{_metric(0, lbl, val)}</g>' 31 51 for i, (lbl, val) in enumerate(metrics) 32 52 ) 33 53 34 - lang_rows = [] 35 - y = 0 54 + lang_title_y = metrics_y + 52 55 + bar_y = lang_title_y + 34 56 + bar_h = 14 57 + segs, x = [], 0.0 36 58 for lang, pct in lang_pcts: 59 + w = pct / 100 * INNER 37 60 color = LANG_COLORS.get(lang, "#858585") 38 - bar = (pct / 100) * 480 39 - lang_rows.append( 40 - f'<g transform="translate(0, {y})">' 41 - f'<circle cx="7" cy="9" r="6.5" fill="{color}"/>' 42 - f'<text class="lang-name" x="22" y="14">{lang}</text>' 43 - f'<rect x="130" y="3" width="{bar:.1f}" height="12" rx="6" fill="{color}" opacity="0.85"/>' 44 - f'<text class="lang-pct" x="{130 + bar + 8:.1f}" y="14">{pct:.0f}%</text>' 61 + segs.append( 62 + f'<rect x="{x:.1f}" y="0" width="{max(w - 2, 1):.1f}" ' 63 + f'height="{bar_h}" rx="3" fill="{color}"/>' 64 + ) 65 + x += w 66 + bar_svg = "".join(segs) 67 + 68 + legend_top = bar_y + bar_h + 26 69 + col_w = INNER / 2 70 + row_h = 25 71 + half = (len(lang_pcts) + 1) // 2 72 + legend = [] 73 + for i, (lang, pct) in enumerate(lang_pcts): 74 + col, row = (0, i) if i < half else (1, i - half) 75 + color = LANG_COLORS.get(lang, "#858585") 76 + legend.append( 77 + f'<g transform="translate({col * col_w:.0f}, {row * row_h})">' 78 + f'<circle cx="6" cy="-4" r="6" fill="{color}"/>' 79 + f'<text class="lg-name" x="20" y="0">{lang}</text>' 80 + f'<text class="lg-pct" x="{col_w - 20:.0f}" y="0" text-anchor="end">{pct:.0f}%</text>' 45 81 f"</g>" 46 82 ) 47 - y += 34 83 + legend_svg = "".join(legend) 84 + height = legend_top + half * row_h + 22 85 + 86 + wave = _waveform(WIDTH - 232, WIDTH - PAD, header_h / 2 + 2, 7, 4) 48 87 49 88 return f'''<svg width="{WIDTH}" height="{height}" viewBox="0 0 {WIDTH} {height}" xmlns="http://www.w3.org/2000/svg"> 50 89 <defs> 51 - <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%"> 52 - <stop offset="0%" style="stop-color:#667eea"/> 53 - <stop offset="100%" style="stop-color:#764ba2"/> 54 - </linearGradient> 55 - <style> 56 - .title {{ font: 600 18px 'Segoe UI', Ubuntu, sans-serif; fill: {t['title']}; }} 57 - .stat-label {{ font: 400 12px 'Segoe UI', Ubuntu, sans-serif; fill: {t['label']}; }} 58 - .stat-value {{ font: 700 22px 'Segoe UI', Ubuntu, sans-serif; fill: {t['value']}; }} 59 - .lang-name {{ font: 400 13px 'Segoe UI', Ubuntu, sans-serif; fill: {t['name']}; }} 60 - .lang-pct {{ font: 600 12px 'Segoe UI', Ubuntu, sans-serif; fill: {t['accent']}; }} 61 - .footer {{ font: 400 11px 'Segoe UI', Ubuntu, sans-serif; fill: {t['label']}; }} 62 - </style> 90 + <clipPath id="card"><rect x="0.5" y="0.5" width="{WIDTH - 1}" height="{height - 1}" rx="6"/></clipPath> 63 91 </defs> 64 - <rect width="{WIDTH}" height="{height}" rx="10" fill="{t['bg']}" stroke="{t['bg_stroke']}" stroke-width="1"/> 65 - <rect x="0" y="0" width="6" height="{height}" rx="3" fill="url(#grad)"/> 66 - <text class="title" x="25" y="38">GitHub Statistics</text> 67 - <g transform="translate(25, 70)">{metric_svg}</g> 68 - <line x1="25" y1="120" x2="{WIDTH - 25}" y2="120" stroke="{t['divider']}" stroke-width="1"/> 69 - <text class="title" x="25" y="160">Most Used Languages</text> 70 - <text class="stat-label" x="25" y="180">Normalized share across repositories (public + private)</text> 71 - <g transform="translate(25, {langs_top})"> 72 - {"".join(lang_rows)} 73 - </g> 74 - <text class="footer" x="{WIDTH // 2}" y="{height - 14}" text-anchor="middle">Updated {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}</text> 92 + <style> 93 + .h-title {{ font: 600 15px {FONT_SANS}; fill: {t['fg']}; }} 94 + .s-title {{ font: 600 14px {FONT_SANS}; fill: {t['fg']}; }} 95 + .s-sub {{ font: 400 11px {FONT_SANS}; fill: {t['muted']}; }} 96 + .m-val {{ font: 600 22px {FONT_MONO}; fill: {t['fg']}; }} 97 + .m-lbl {{ font: 400 11px {FONT_SANS}; fill: {t['muted']}; }} 98 + .lg-name {{ font: 400 13px {FONT_SANS}; fill: {t['fg']}; }} 99 + .lg-pct {{ font: 500 13px {FONT_MONO}; fill: {t['muted']}; }} 100 + .foot {{ font: 400 10px {FONT_MONO}; fill: {t['muted']}; }} 101 + </style> 102 + <rect x="0.5" y="0.5" width="{WIDTH - 1}" height="{height - 1}" rx="6" fill="{t['bg']}"/> 103 + <g clip-path="url(#card)"><rect x="0" y="0" width="{WIDTH}" height="{header_h}" fill="{t['bg_muted']}"/></g> 104 + <line x1="0.5" y1="{header_h}" x2="{WIDTH - 0.5}" y2="{header_h}" stroke="{t['border']}"/> 105 + <rect x="0.5" y="0.5" width="{WIDTH - 1}" height="{height - 1}" rx="6" fill="none" stroke="{t['border']}"/> 106 + <path d="{wave}" fill="none" stroke="{t['accent']}" stroke-width="1.5" opacity="0.45"/> 107 + <text class="h-title" x="{PAD}" y="31">GitHub Statistics</text> 108 + <g transform="translate({PAD}, {metrics_y})">{metric_svg}</g> 109 + <text class="s-title" x="{PAD}" y="{lang_title_y}">Most Used Languages</text> 110 + <text class="s-sub" x="{PAD}" y="{lang_title_y + 17}">Normalized share across repositories · public + private</text> 111 + <g transform="translate({PAD}, {bar_y})">{bar_svg}</g> 112 + <g transform="translate({PAD}, {legend_top})">{legend_svg}</g> 113 + <text class="foot" x="{WIDTH // 2}" y="{height - 11}" text-anchor="middle">updated {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}</text> 75 114 </svg>'''
+25 -8
statsgen/theme.py
··· 1 - """Tema visual compartido (colores de tema y de lenguaje).""" 1 + """Tokens de tema alineados con GitHub Primer (claro y oscuro). 2 + 3 + Los valores se extrajeron de las variables CSS reales de github.com 4 + (--bgColor-*, --borderColor-*, --fgColor-*) para que los paneles generados 5 + se integren como una caja nativa más de GitHub en ambos temas. 6 + """ 7 + 8 + # Stacks de fuente de GitHub. En un SVG renderizado como <img> no se carga 9 + # la webfont "Mona Sans", así que se cae al mismo stack de sistema que usa 10 + # GitHub como fallback; los datos van en monoespaciada (estética de readout). 11 + FONT_SANS = ( 12 + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', " 13 + "Helvetica, Arial, sans-serif" 14 + ) 15 + FONT_MONO = ( 16 + "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, " 17 + "'Liberation Mono', monospace" 18 + ) 2 19 3 20 THEMES = { 4 21 "dark": { 5 - "bg": "#0d1117", "bg_stroke": "#30363d", "title": "#c9d1d9", 6 - "label": "#8b949e", "value": "#c9d1d9", "name": "#8b949e", 7 - "accent": "#c9d1d9", "divider": "#30363d", 22 + "bg": "#0d1117", "bg_muted": "#151b23", "border": "#3d444d", 23 + "fg": "#f0f6fc", "muted": "#9198a1", "accent": "#4493f8", 24 + "success": "#3fb950", 8 25 }, 9 26 "light": { 10 - "bg": "#ffffff", "bg_stroke": "#e1e4e8", "title": "#24292e", 11 - "label": "#586069", "value": "#24292e", "name": "#586069", 12 - "accent": "#24292e", "divider": "#e1e4e8", 27 + "bg": "#ffffff", "bg_muted": "#f6f8fa", "border": "#d1d9e0", 28 + "fg": "#1f2328", "muted": "#59636e", "accent": "#0969da", 29 + "success": "#1a7f37", 13 30 }, 14 31 } 15 32 16 - # Colores oficiales de GitHub Linguist por lenguaje (para los puntos y barras). 33 + # Colores oficiales de GitHub Linguist por lenguaje (puntos y barra). 17 34 LANG_COLORS = { 18 35 "C++": "#f34b7d", "C": "#555555", "Python": "#3572A5", "MATLAB": "#e16737", 19 36 "JavaScript": "#f1e05a", "Shell": "#89e051", "TypeScript": "#2b7489",
+8 -3
tests/test_render_blog.py
··· 14 14 assert svg.lstrip().startswith("<svg") 15 15 16 16 17 - def test_card_contains_index_and_read_more(): 17 + def test_card_marks_latest_and_shows_date_and_read_more(): 18 18 svg = render_blog_card_svg(POST, index=1, theme="dark") 19 - assert "01" in svg 20 - assert "Jun 2026" in svg 19 + assert "LATEST" in svg # index 1 = entrada más reciente 20 + assert "JUN 2026" in svg # fecha como eyebrow en mayúsculas 21 21 assert "Read more" in svg 22 + 23 + 24 + def test_card_without_latest_for_non_first(): 25 + svg = render_blog_card_svg(POST, index=2, theme="dark") 26 + assert "LATEST" not in svg 22 27 23 28 24 29 def test_card_escapes_xml_special_chars():