{
  "header.eyebrow": "var x = 'Portfolio'; // x because 'portfolio' was too long to type",
  "header.title": "document.title = getTitle() || getTitle2() || getTitleBackup() || 'Projets'; // fallback chain of broken functions",
  "header.description": "/* This description was written by 4 developers over 3 years. Each one added a sentence without reading the others. It contradicts itself twice. The grammar checker crashed. We shipped it anyway. It's held together by string concatenation and hope. */",
  "header.cta_works.top": "// the button that opens 14 modals",
  "header.cta_works.bottom": "// redirects 3 times, nobody knows why",
  "filter.category": "switch(cat) { case 'all': case 'personal': case 'open_source': case 'client': default: showAll(); break; } // every case does the same thing",
  "filter.tags": "var tags = tags || tag || Tags || TAGS || window.tags || []; // we WILL find the tags",
  "filter.stack": "var stack = JSON.parse(localStorage.getItem('stack') || sessionStorage.getItem('stack') || cookies.get('stack') || 'null') || []; // checked every storage mechanism known to man",
  "filter.reset": "location.href = location.href.split('?')[0].split('#')[0] + '?reset=true&really=true&please=true'; // reset via query params. 3 of them. for emphasis.",
  "filter.search_tag": "function searchTag(q) { return allTags.filter(t => t.includes(q) || t.startsWith(q) || t.endsWith(q) || t == q || q.includes(t) || true); } // matches everything. just in case.",
  "filter.search_stack": "// search_stack: copied from search_tag. changed variable names. forgot to change the error message. it says 'no tag found' when stacks are empty.",
  "filter.none_tag": "console.error('no tags lol'); // the error message IS the UI",
  "filter.none_stack": "alert('Aucune stack trouvée.'); // alert() for 'no results'. UX perfection.",
  "filter.selected": "element.className += ' selected'; element.className += ' selected'; // added twice. removing one breaks the CSS. specificity hack.",
  "filter.all": "var all = true; // 'all' is a boolean. it's always true. if it's false, we set it to true.",
  "filter.personal": "var personal = !professional; // derived from the negation of another variable that's also undefined",
  "filter.open_source": "var opensource = {}; opensource.open = true; opensource.source = true; opensource.isOpenSource = opensource.open && opensource.source; // boolean algebra for a label",
  "filter.client": "// client filter: disabled since 2022. the if-block is still there. it checks (false && client).",
  "status.done": "var done = 'done'; if(done == 'done') done = 'really done'; if(done == 'really done') done = 'truly done'; // escalating doneness",
  "status.in_progress": "// TODO TODO TODO TODO TODO TODO TODO TODO (one TODO per sprint it wasn't done)",
  "status.archived": "// archived: moved to archived_projects_old_backup_DONT_DELETE/",
  "omnicard.shortDescription": "var game = new Object(); game.type = 'card'; game.strategy = true; game.custom = true; // object built property by property. 47 more properties follow.",
  "omnicard.longDescription": "/* Omnicard codebase archaeology report: Line 1: 'use strict' (commented out). Line 2-47: variable declarations (var a through var az). Line 48: function playCard(a, b, c, d, e, f, g, h, i, j, k) — 11 parameters, 3 are used. Line 200: the game loop. It's a setInterval(gameLoop, 16.666). The 16.666 is hardcoded. If the monitor isn't 60Hz, the game runs at the wrong speed. Line 847: 'TODO: add networking'. Line 848: commented-out WebSocket code from a tutorial. Line 849: game state synced via localStorage across tabs. That's the multiplayer. Open 2 tabs. That's multiplayer. Lines 850-14000: a single switch statement for card effects. Each case is 20-200 lines. There are 400 cases. Nobody has read them all. A code review would take 3 weeks. We've scheduled it 6 times. */",
  "omnicard.highlights.0": "function RuleEngine() { eval(localStorage.getItem('rules')); } // the rule engine evaluates arbitrary strings. by design.",
  "omnicard.highlights.1": "var state = {}; /* ...3000 lines later... */ state.player1.hand[0].effects[2].subeffect.trigger(); // 8 levels deep. the debugger cries.",
  "omnicard.highlights.2": "window.addEventListener('storage', syncGame); // multiplayer via localStorage events. open another tab. you're in a match.",
  "omnicard.lessonsLearned.0": "// Learned: eval() for game rules was 'faster to implement'. also faster to exploit.",
  "omnicard.lessonsLearned.1": "// Learned: a 14,000 line switch statement is technically a finite state machine",
  "omnicard.lessonsLearned.2": "// Learned: localStorage multiplayer has a max of 2 players (2 tabs). we call this a 'design choice'.",
  "pvzf-translation-fr.shortDescription": "var isLead = window.location.href.includes('charles'); // leadership determined by URL",
  "pvzf-translation-fr.longDescription": "/* Translation pipeline (spaghetti edition): 1) Read source JSON with eval('(' + fileContent + ')') because JSON.parse is 'too strict'. 2) Compare keys via nested for loops: for(var i in a) for(var j in b) if(i == j)... 3) Missing translations stored in a global array called 'temp' (it's been 'temp' for 2 years). 4) Quality check: if(translated.length > 0) { quality = 'good'; } 5) Terminology consistency enforced by a 300-line if/else chain. if(word=='player') return 'joueur'; else if(word=='Player') return 'Joueur'; else if(word=='PLAYER') return 'JOUEUR'; // case sensitive. each case. manually. 6) Lead review: me reading the diff at 2am and approving with '👍' as the commit message */",
  "pvzf-translation-fr.highlights.0": "if(username == 'charles') { isLead = true; } else { isLead = false; } // role based access control: one if statement",
  "pvzf-translation-fr.highlights.1": "var consistency = (a == b) ? 'consistent' : 'inconsistent'; // the entire quality framework",
  "pvzf-translation-fr.highlights.2": "var contributions = []; contributions.push(c); contributions.push(c); // every contribution counted twice. nobody complained.",
  "pvzf-translation-fr.lessonsLearned.0": "// Learned: eval() for JSON parsing is 'flexible'. also 'dangerous'. mainly 'dangerous'.",
  "pvzf-translation-fr.lessonsLearned.1": "// Learned: a 300-line if/else for terminology could be a Map. could be. isn't.",
  "pvzf-translation-fr.lessonsLearned.2": "// Learned: '👍' is not a valid commit message but git doesn't complain",
  "portfolio.shortDescription": "var portfolio = this; // 'this' refers to... depends on how you called it. could be window. could be undefined. could be Shrek.",
  "portfolio.longDescription": "/* This portfolio's codebase: app.js (1 file, 8,000 lines). It does: routing (manual if/else on window.location.hash), state management (47 global variables), animations (setInterval everywhere, clearInterval nowhere), audio (new Audio() created on every click, never garbage collected, RAM usage grows linearly with time spent on site), SEO (a comment that says '<!-- TODO: SEO -->'), and responsive design (one @media query: @media(max-width:768px) { * { font-size: 8px; } }). The CSS file is 4,000 lines, has 200 !important declarations, and starts with '/* DO NOT MODIFY BELOW THIS LINE' on line 1. The git history has 847 commits. 400 are 'fix'. 200 are 'fix fix'. The rest are merge commits from rebases gone wrong. */",
  "portfolio.highlights.0": "window.onhashchange = function() { eval(location.hash.slice(1)); } // routing via eval on URL hash. type #alert('hacked') in the URL bar.",
  "portfolio.highlights.1": "var responsive = (window.innerWidth > 768) ? 'desktop' : 'mobile'; // calculated once. on page load. never again. resize? refresh.",
  "portfolio.highlights.2": "setInterval(animate, 16); setInterval(animate, 16); // registered twice. double speed animations. 'feature'.",
  "portfolio.highlights.3": "/* Lighthouse score: we don't run Lighthouse. we run from Lighthouse. */",
  "portfolio.highlights.4": "document.body.style.cssText = 'margin:0;padding:0;font-family:Bloodborne,serif;'; // design system: one inline style on body",
  "portfolio.lessonsLearned.0": "// Learned: eval(location.hash) is routing AND a security vulnerability. two for one.",
  "portfolio.lessonsLearned.1": "// Learned: never-cleared setIntervals are memory leaks with personality",
  "portfolio.lessonsLearned.2": "// Learned: !important x200 means nobody can override anything. including you.",
  "portfolio.lessonsLearned.3": "// Learned: 'responsive' means more than one @media query that sets everything to 8px",
  "lis-web.shortDescription": "var professional = true; var amateur = true; // both are true. schrodinger's developer.",
  "lis-web.longDescription": "/* LIS Web: the professional project where professionalism went to die. Requirements doc: a WhatsApp voice memo. Design mockup: a photo of a napkin. Timeline: 'ASAP but also take your time'. The codebase: jQuery spaghetti with inline PHP in JavaScript strings inside HTML attributes. onclick=\"<?php echo getAction(); ?>\". The deployment: someone renamed the local folder and copied it to a USB drive. The USB drive was plugged into the production server. The production server is under someone's desk. It's still there. The monitoring: 'call me if the site goes down' - the client, who has no internet at home. */",
  "lis-web.highlights.0": "onclick=\"<?php echo $action; ?>\" // PHP inside JS inside HTML inside an attribute. the full turducken.",
  "lis-web.highlights.1": "// alignment: the logo is 3px off-center. the client noticed. I can't find why. it haunts me.",
  "lis-web.highlights.2": "var real_project = real_project || true; // it's real. we keep reminding ourselves.",
  "lis-web.lessonsLearned.0": "// Learned: voice memos are not requirements documents. even if you transcribe them.",
  "lis-web.lessonsLearned.1": "// Learned: a napkin design loses details when you spill coffee on it",
  "lis-web.lessonsLearned.2": "// Learned: USB-drive deployment is called 'sneakernet'. it has a Wikipedia page. it's still bad.",
  "dev-mates.shortDescription": "var company = { name: 'Dev-Mates', impressive: true, website: 'TODO' }; // the website was TODO for 8 months",
  "dev-mates.longDescription": "/* Dev-Mates evolution, in code: v1: document.write('<h1>Dev-Mates</h1><p>We make websites</p>'); // entire company website. v2: added jQuery for a fade-in effect on the title. jQuery version: 1.4. Still on CDN. The CDN might remove it any day. v3: WordPress. The theme was 'Developer starter theme (DO NOT USE IN PRODUCTION)'. We used it in production. v4: 'Let me just quickly rewrite this in React'. It was neither quick nor in React. It ended up as vanilla JS with JSX comments. v5: Angular. Finally. Properly. The previous 4 versions are in folders named 'old', 'old2', 'backup', and 'dont-look-here'. They're all in the same Git repo. The repo is 2.4GB. The actual code is 200KB. */",
  "dev-mates.highlights.0": "document.title = 'Dev-Mates | ' + 'Professional | ' + 'Trust | ' + 'Please | ' + 'Hire Us'; // SEO via increasingly desperate title tags",
  "dev-mates.highlights.1": "if(message && image && structure) { coherent = 'yes'; } else { coherent = 'close enough'; } // coherence: approximate",
  "dev-mates.highlights.2": "var credibility = reputation++ + expertise++ + (Math.random() > 0.5 ? 'bonus' : 0); // credibility score includes a random bonus. NaN half the time.",
  "dev-mates.lessonsLearned.0": "// Learned: 'DO NOT USE IN PRODUCTION' means exactly that. who knew.",
  "dev-mates.lessonsLearned.1": "// Learned: 4 rewrites before the right stack is called 'iteration'. not 'indecision'.",
  "dev-mates.lessonsLearned.2": "// Learned: a 2.4GB repo that's 200KB of code is 2.3999GB of regret",
  "pvz-fuzion-console-manager.shortDescription": "exec('node check.js || python check.py || bash check.sh || echo \"good enough\"'); // tries every language until one works",
  "pvz-fuzion-console-manager.longDescription": "/* The translation checker codebase: File 1: check.js (the original, 47 lines, clear, readable). File 2: check_v2.js (added by the intern, 200 lines, unclear, unreadable). File 3: check_v2_fixed.js (fixed the intern's version, 400 lines, technically works). File 4: check_v3_FINAL.js (rewrote from scratch, 800 lines, broke backward compatibility). File 5: check_v2_fixed_v2.js (went back to v2, fixed again, 500 lines). Currently running: check.js (the original 47-line version). Files 2-5 are still in the repo. They have test files. The tests pass. For the wrong files. Nobody has noticed because the CI only runs check.js. The check itself: JSON.parse, Object.keys, compare. 3 lines. The other 44 lines are console.log formatting. */",
  "pvz-fuzion-console-manager.highlights.0": "var regex = /^(?:(?:[a-z](?:_[a-z])*)\\.){0,5}(?:[a-z](?:_[a-z])*)$/gim; // regex that matches translation keys. or phone numbers. unclear.",
  "pvz-fuzion-console-manager.highlights.1": "console.log('\\x1b[32m' + 'PASS' + '\\x1b[0m'); // quality control = green text in terminal",
  "pvz-fuzion-console-manager.highlights.2": "process.exit(0); // always exits 0. even on failure. CI always green. always.",
  "pvz-fuzion-console-manager.lessonsLearned.0": "// Learned: 5 versions of the same script is called 'evolutionary architecture' on your CV",
  "pvz-fuzion-console-manager.lessonsLearned.1": "// Learned: process.exit(0) on failure is 'optimistic testing'",
  "pvz-fuzion-console-manager.lessonsLearned.2": "// Learned: 3 lines of logic + 44 lines of console.log = enterprise code",
  "shreksophone.shortDescription": "document.body.innerHTML = '<video src=shrek.mp4 autoplay loop>'; // the entire app. one line. perfection.",
  "shreksophone.longDescription": "/* Shreksophone source code (complete, unabridged): document.body.innerHTML = ''; var v = document.createElement('video'); v.src = 'shrek_sax.mp4'; v.autoplay = true; v.loop = true; v.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;object-fit:cover;z-index:99999999;'; document.body.appendChild(v); // That's it. 7 lines. No framework. No build step. No webpack. No node_modules. No package.json. No dependencies. No vulnerabilities. No tech debt. No meetings about it. It just works. It has never had a bug. It has never needed a hotfix. It is the most stable software I have ever written. The z-index is 99999999 because it must be above everything. Including your hopes. Including your CSS reset. Including other Shreksophone instances. Shrek is eternal. Shrek is above all. */",
  "shreksophone.highlights.0": "z-index: 99999999 // because 9999999 wasn't enough. one more 9. for Shrek.",
  "shreksophone.highlights.1": "v.autoplay = true; v.loop = true; // two booleans. zero consent. maximum ogre.",
  "shreksophone.highlights.2": "// creative direction: 'yes'. full stop. no design review needed. shrek IS the design.",
  "shreksophone.lessonsLearned.0": "// Learned: the best code is the code with no dependencies. also: the code is 7 lines of Shrek.",
  "shreksophone.lessonsLearned.1": "// Learned: z-index wars end at 99999999. or do they.",
  "shreksophone.lessonsLearned.2": "// Learned: 0 bugs, 0 tech debt, 0 frameworks. turns out Shrek was the answer all along.",
  "glossairequest.shortDescription": "var quiz = { secure: 'md5 counts right?', tested: 'the users are the tests' };",
  "glossairequest.longDescription": "/* GlossaireQuest authentication system: function login(user, pass) { if(users[user] == md5(pass)) return true; } That's it. That's the auth. No salt. No pepper. No bcrypt. No rate limiting. No account lockout. No CAPTCHA. Just md5 and vibes. The password reset: email a link with ?user=admin&newpass=reset123 in the URL. In plaintext. Over HTTP. The admin panel: /admin (not /admin.php. we upgraded from PHP, remember?). Auth check: if(localStorage.getItem('isAdmin') == 'true'). Yes. Client-side. Yes. A string comparison. Yes. You can open DevTools and type localStorage.setItem('isAdmin', 'true'). Yes. Someone did. */",
  "glossairequest.highlights.0": "if(localStorage.getItem('isAdmin') == 'true') // JWT: Just Trust localStorage",
  "glossairequest.highlights.1": "document.cookie = 'score=' + score + '; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT'; // score cookie expires in 7973 years. persistent.",
  "glossairequest.highlights.2": "import { QuizComponent } from './quiz'; import { QuizComponent } from '../quiz'; // imported twice from different paths. somehow both resolve. somehow.",
  "glossairequest.lessonsLearned.0": "// Learned: localStorage.getItem('isAdmin') is not 'zero trust'. it's 'maximum trust'. in the wrong entity.",
  "glossairequest.lessonsLearned.1": "// Learned: cookies with expiry year 9999 are basically permanent. cockroaches and quiz scores will survive the apocalypse.",
  "glossairequest.lessonsLearned.2": "// Learned: if a user can edit their own score in DevTools, your leaderboard is just a creative writing exercise",
  "league-of-data-base.shortDescription": "fetch(RIOT_API).then(r => r.json()).then(d => { window.champions = d; }); // global variable populated by async fetch. race condition as a feature.",
  "league-of-data-base.longDescription": "/* League of Data Base (the name is a pun and I'm not sorry): Data fetching: fetch() in a setInterval(). Every 30 seconds. No cache. No delta. Full dataset. Every time. The Riot API key is hardcoded in the frontend JavaScript. View Source reveals it. Someone on Reddit found it. They were nice about it. The i18n: a 2000-line object literal. { 'Aatrox': { 'fr': 'Aatrox', 'de': 'Aatrox', 'es': 'Aatrox' } }. 90% of champion names are the same across languages. We mapped them all. It took 2 weeks. The storage optimization (hard links): genuinely good. Credit where it's due. One good decision in a sea of chaos. We reference it in every code review as proof we can make good decisions. */",
  "league-of-data-base.highlights.0": "const API_KEY = 'RGAPI-12345678-dead-beef-cafe-123456789abc'; // hardcoded in frontend JS. viewable via F12. educational.",
  "league-of-data-base.highlights.1": "var hardlink = true; // the ONE good decision. framed on the wall.",
  "league-of-data-base.highlights.2": "document.write('<table>'); for(var i in champions) { document.write('<tr><td>' + champions[i].name + '</td></tr>'); } // document.write + for-in loop + table. the holy trinity of 2004.",
  "league-of-data-base.lessonsLearned.0": "// Learned: API keys in frontend JS are 'open source'. involuntarily.",
  "league-of-data-base.lessonsLearned.1": "// Learned: hard links are great. we bring this up every sprint as evidence of competence.",
  "league-of-data-base.lessonsLearned.2": "// Learned: mapping 'Aatrox' to 'Aatrox' in 10 languages is 'thorough'. not 'wasted time'.",
  "blender-collection.shortDescription": "var addons = []; addons.push = function() { addons[addons.length] = arguments[0]; addons.length++; }; // reimplemented Array.push. for control.",
  "blender-collection.longDescription": "/* Blender Collection: where Array methods go to be reimplemented. We have custom push(), pop(), filter(), map(), and sort(). Not because the native ones don't work. Because the original dev 'didn't trust built-in functions'. The sort function: function sort(arr) { for(var i=0;i<arr.length;i++) for(var j=0;j<arr.length;j++) if(arr[i]<arr[j]) { var temp=arr[i]; arr[i]=arr[j]; arr[j]=temp; } } Yes, it's bubble sort. No, it doesn't have a name. It's in a file called 'utils.js' alongside 47 other unlabeled functions. The download feature: creates a zip by concatenating file contents with '---FILE_SEPARATOR---' between them. The 'unzip' on the client: split('---FILE_SEPARATOR---'). It's not a real zip. Users complained. We renamed the button from 'Download ZIP' to 'Download Collection'. Problem solved. */",
  "blender-collection.highlights.0": "if(role == 'admin' || role == 'Admin' || role == 'ADMIN' || role == 'aDmIn') // case-insensitive comparison the hard way",
  "blender-collection.highlights.1": "function analytics() { return document.querySelectorAll('*').length; } // analytics: count all DOM nodes. that's the metric.",
  "blender-collection.highlights.2": "setInterval(processQueue, 5000); // the worker. runs every 5s. has been running since deploy. will run forever. consumes 30% CPU doing nothing 99% of the time.",
  "blender-collection.lessonsLearned.0": "// Learned: reimplementing Array.push 'for control' is called 'job security'. also 'insanity'.",
  "blender-collection.lessonsLearned.1": "// Learned: '---FILE_SEPARATOR---' is not a file format. renaming the button doesn't fix this.",
  "blender-collection.lessonsLearned.2": "// Learned: counting DOM nodes is not analytics. but the graph goes up, so management is happy.",
  "symfony-session.shortDescription": "sessionStorage.setItem('session', JSON.stringify(sessionStorage)); // storing sessionStorage inside sessionStorage. inception.",
  "symfony-session.longDescription": "/* Symfony Session: The Session Management Hall of Shame. Session #1: Login state stored in URL: /dashboard?logged_in=true. Change to false: logged out. Session #2: Auth token stored in a cookie named 'auth'. Value: 'true'. Not a JWT. Not a session ID. The string 'true'. Session #3: User role stored in localStorage. DevTools > Application > localStorage > role: 'admin'. Anyone can be admin. Session #4: CSRF protection: a hidden input with value '1'. Always '1'. For every form. For every user. Session #5: Remember me: a cookie that never expires. Contains the username and md5(password). In cleartext. In the cookie name. The cookie is called 'remember_admin_e10adc3949ba59abbe56e057f20f883e'. The md5 IS the password hash. View > Cookies reveals credentials. We are aware. The Jira ticket is marked 'Low Priority'. */",
  "symfony-session.highlights.0": "var captcha = prompt('Type CAPTCHA: ' + Math.random().toString(36).slice(2)); // CAPTCHA via prompt(). bots are confused. users are also confused.",
  "symfony-session.highlights.1": "for(var i=0; i<users.length; i++) { document.write('<tr>'); for(var j=0; j<fields.length; j++) { document.write('<td>' + users[i][fields[j]] + '</td>'); } } // CRUD: rendered via nested for loops and document.write",
  "symfony-session.highlights.2": "window.print(); // PDF generation: call window.print(). the browser IS the PDF engine.",
  "symfony-session.lessonsLearned.0": "// Learned: /dashboard?logged_in=true is not authentication. it's a suggestion.",
  "symfony-session.lessonsLearned.1": "// Learned: CSRF token '1' protects against exactly zero attacks",
  "symfony-session.lessonsLearned.2": "// Learned: window.print() as PDF generation is either genius or resignation. both.",
  "timeline.featured": "element.style.border = '3px solid red'; element.style.border = '3px solid gold'; // couldn't decide. gold wins. last write wins.",
  "timeline.detail": "window.open(window.open(url)); // double open. first one is the detail. second one is... bonus?",
  "timeline.aria": "// timeline aria: <div role='list'> containing <span role='listitem'> containing <div role='button'> containing <a role='link'>. aria roles: collected them all.",
  "modal.close": "modal.style.display='none'; modal.style.visibility='hidden'; modal.style.opacity='0'; modal.style.height='0'; modal.style.width='0'; modal.remove(); // 6 ways to hide. all applied. just to be sure.",
  "modal.image_fullscreen": "document.body.innerHTML = '<img src=\"'+img+'\">'; // fullscreen: delete everything else",
  "modal.previous": "history.back(); history.back(); history.forward(); // net result: back 1. the forward is accidental. removing it breaks things.",
  "modal.next": "currentIndex = currentIndex + 1; if(currentIndex > max) currentIndex = currentIndex; // overflow: do nothing. display broken image. user assumes it's art.",
  "modal.video_title": "document.title = 'VIDEO'; // the video title is... the page title. changed globally. stays after closing the modal.",
  "modal.description": "innerHTML += description; innerHTML += description; // description appended twice. scroll to see the duplicate. or refresh.",
  "modal.lessons": "var lessons = lessons; // assigned to itself. still passes the null check somehow. JavaScript.",
  "modal.highlights": "highlights.forEach(h => document.write(h)); // document.write inside forEach inside a modal inside a SPA. if this renders, it's a miracle.",
  "modal.links": "// links: <a href='javascript:void(0)' onclick='window.open(url)'>. every link opens a popup. every popup is blocked. zero links work.",
  "modal.demo": "// demo link: points to localhost:4200. works on my machine. only on my machine.",
  "modal.site": "// site link: href is a relative path '../../../site/index.html'. resolves to /index.html. which is this site. clicking 'site' opens this page. recursive portfolio.",
  "today": "var today = new Date().getFullYear() + '-' + (new Date().getMonth()+1) + '-' + new Date().getDate(); // 3 new Date() calls. what if midnight strikes between them? different days. that's the fun."
}
