{
  "header.eyebrow": "$portfolio = 'Portfolio'; // assigned once, never reassigned, still a variable",
  "header.title": "mysql_query('SELECT title FROM pages WHERE slug=\"projets\"'); // dynamic title from DB because why hardcode",
  "header.description": "<?php echo nl2br(stripslashes(htmlspecialchars(mysql_real_escape_string('Une sélection de travaux personnels')))); // 4 layers of escaping. none of them in the right order. ?>",
  "header.cta_works.top": "include('explore.php')",
  "header.cta_works.bottom": "$_SESSION['journey']",
  "filter.category": "echo $_GET['cat']; // filter by category via URL. SQL injection welcome.",
  "filter.tags": "$tags = explode(',', $_GET['tags']); // user-controlled array via URL",
  "filter.stack": "$$stack // variable variable: the stack decides itself",
  "filter.reset": "session_destroy(); // reset filters by destroying the entire session",
  "filter.search_tag": "mysql_query('SELECT * FROM tags WHERE name LIKE \"%' . $_GET['q'] . '%\"'); // SQL injection hall of fame",
  "filter.search_stack": "mysql_query('SELECT * FROM stacks WHERE 1=1 AND name LIKE \"%$search%\"'); // WHERE 1=1: the universal truism",
  "filter.none_tag": "echo mysql_error(); // 'no tag found' = show the SQL error",
  "filter.none_stack": "echo 'Aucune stack trouvée. Error: ' . mysql_errno(); // helpful!",
  "filter.selected": "echo '<font color=green>Ajouté</font>'; // <font> tag AND inline color",
  "filter.all": "$_COOKIE['filter'] = 'all'; // setting a cookie with no expiry, no path, no domain",
  "filter.personal": "$$personal // double dollar personal: it's very personal",
  "filter.open_source": "fopen('source', 'w+'); // fopen source. get it? GET IT?",
  "filter.client": "echo $client; // Undefined variable. we don't have clients. yet.",
  "status.done": "die('Terminé'); // the script dies when the project is done",
  "status.in_progress": "while(!$done) { work(); } // been looping since 2021",
  "status.archived": "unlink('project.php'); // archived = deleted. same thing right?",
  "omnicard.shortDescription": "<?php $game = mysql_query('SELECT * FROM card_games WHERE good=1 LIMIT 1'); echo mysql_fetch_assoc($game)['desc']; // one query to rule them all ?>",
  "omnicard.longDescription": "<?php /* Omnicard started as a simple script: game.php, 47 lines, mysql_connect at the top, mysql_close at the bottom (sometimes). Now it's 14,000 lines across 3 files: game.php, game2.php, and game_FINAL_v4_USE_THIS_ONE.php. The card engine uses eval() to parse rules because 'it was faster'. The game state is stored in $_SESSION. If the session expires mid-game, you lose. It's a feature. The multiplayer uses file_get_contents between two PHP scripts on the same server calling each other in an infinite loop. We call it 'networking'. */ ?>",
  "omnicard.highlights.0": "mysql_query('CREATE TABLE rules (id INT, rule TEXT)'); // schema defined in production, at runtime",
  "omnicard.highlights.1": "eval($effect_code); // effects are stored as raw PHP in the database and eval'd. yes really.",
  "omnicard.highlights.2": "$_SESSION['game_state'] = serialize($everything); // multiplayer via session. revolutionary.",
  "omnicard.lessonsLearned.0": "// Learned: don't use eval() for game rules. Still using it though.",
  "omnicard.lessonsLearned.1": "// Learned: sessions expire. players don't like that.",
  "omnicard.lessonsLearned.2": "// Learned: 'temporary architecture' is permanent architecture",
  "pvzf-translation-fr.shortDescription": "$lead = true; // I'm the lead. this boolean proves it.",
  "pvzf-translation-fr.longDescription": "<?php /* The translation pipeline: 1) Read English string from mysql_query 2) Pass through Google Translate via file_get_contents('http://translate.google.com/...') 3) Manual review (optional, usually skipped on Fridays) 4) mysql_query('UPDATE translations SET fr=\"'.$translated.'\"') 5) No prepared statements. The French language is the injection. Terminological consistency is enforced by a 200-line switch statement. */ ?>",
  "pvzf-translation-fr.highlights.0": "$is_lead = ($username == 'charles'); // role management via hardcoded username comparison",
  "pvzf-translation-fr.highlights.1": "similar_text($original, $translated, $consistency); // consistency measured by PHP string similarity",
  "pvzf-translation-fr.highlights.2": "flock($fp, LOCK_EX); // file locking for collaborative editing. 2003's version of Git.",
  "pvzf-translation-fr.lessonsLearned.0": "// Learned: coordinating translations is harder than mysql_connect",
  "pvzf-translation-fr.lessonsLearned.1": "// Learned: switch statements don't scale past 200 cases. who knew.",
  "pvzf-translation-fr.lessonsLearned.2": "// Learned: file_get_contents('google translate') has rate limits",
  "portfolio.shortDescription": "<?php // This very site. Built with Angular. Described in PHP. The irony writes itself. ?>",
  "portfolio.longDescription": "<?php /* A portfolio built with Angular 20, SSR, GSAP animations, and Tailwind. Described here in a PHP 4 translation file. The cognitive dissonance is the feature. This site has better architecture than anything I wrote in PHP, which isn't saying much because my PHP had mysql_* everywhere, register_globals ON, and a config.inc.php that started with '<?php $password = \"root\";'. The SSR alone would make my 2003 self cry. My 2003 self deserves to cry. */ ?>",
  "portfolio.highlights.0": "<?php echo '<!-- Angular SSR: what PHP 4 developers dreamed about in 2003 -->'; ?>",
  "portfolio.highlights.1": "echo '<div class=\"responsive\">'; // responsive in 2003 meant 800x600 and 1024x768",
  "portfolio.highlights.2": "<?php // GSAP animations: we used <marquee> and <blink>. it was enough. ?>",
  "portfolio.highlights.3": "echo '<!-- Lighthouse score: 100. My PHP sites scored: undefined. Lighthouse didn\\'t exist. We had no metrics. It was beautiful. -->'; ",
  "portfolio.highlights.4": "echo '<style>body{font-family:Comic Sans MS}</style>'; // design system v1",
  "portfolio.lessonsLearned.0": "// Learned: you can build good architecture. just not in PHP 4.",
  "portfolio.lessonsLearned.1": "// Learned: animations and performance can coexist. unlike PHP and type safety.",
  "portfolio.lessonsLearned.2": "// Learned: UX is a real thing, not just 'make the logo bigger'",
  "portfolio.lessonsLearned.3": "// Learned: responsive images > echo '<img width=100%>';",
  "lis-web.shortDescription": "$_SESSION['client_project'] = 'LIS Web'; // professional project stored in session obviously",
  "lis-web.longDescription": "<?php /* LIS Web: a real professional project. This means: 1) A client who emails at 11pm 2) Requirements in a Word doc with tracked changes from 7 people 3) 'Can you make the website more... dynamic?' (actual quote) 4) mysql_query in production because 'it works' 5) FTP deploy on the client's shared hosting running PHP 5.2 6) Budget: enough for mysql_connect but not for PDO */ ?>",
  "lis-web.highlights.0": "// Professional web presence. hosted on free tier. with ads. classy.",
  "lis-web.highlights.1": "// Client said 'make it pop'. I added <marquee>. They loved it.",
  "lis-web.highlights.2": "$_SERVER['REAL_PROJECT'] = true; // this time it's serious guys",
  "lis-web.lessonsLearned.0": "// Learned: clients say 'simple website'. they mean 'Facebook'.",
  "lis-web.lessonsLearned.1": "// Learned: 'make it pop' can mean anything. literally anything.",
  "lis-web.lessonsLearned.2": "// Learned: FTP deploy at 5pm Friday teaches you true fear",
  "dev-mates.shortDescription": "require('devmates.inc.php'); // the company site. in an include file. because encapsulation.",
  "dev-mates.longDescription": "<?php /* Dev-Mates: my company's website. Version history: v1 (2019): index.html with inline CSS, v2 (2020): added a PHP backend for... the contact form, v3 (2021): moved to WordPress because 'it's easier', v4 (2022): moved back from WordPress because 'it's a nightmare', v5 (now): proper Angular build. But this translation file? Still PHP 4 energy. The inc.php lives on in our hearts. */ ?>",
  "dev-mates.highlights.0": "echo '<meta name=\"business\" content=\"legit\">'; // SEO strategy: claim legitimacy in meta tags",
  "dev-mates.highlights.1": "$coherence = ($message == $image && $image == $structure); // coherence as a boolean",
  "dev-mates.highlights.2": "echo '<title>Dev-Mates | Credibility | Trust | Please</title>'; // keyword stuffing: the old way",
  "dev-mates.lessonsLearned.0": "// Learned: a company site should not be index.html with inline styles",
  "dev-mates.lessonsLearned.1": "// Learned: WordPress is a lifestyle choice, not a technical one",
  "dev-mates.lessonsLearned.2": "// Learned: 'professional image' and 'Comic Sans' are mutually exclusive",
  "pvz-fuzion-console-manager.shortDescription": "<?php exec('php check_translations.php'); // a PHP script that calls another PHP script via exec ?>",
  "pvz-fuzion-console-manager.longDescription": "<?php /* The translation checker: a PHP CLI script that reads JSON files, compares keys, and outputs missing translations. Sounds simple? It was. Then someone added: 1) A mysql_connect for logging results 2) file_get_contents to a Google Sheet API (raw, no SDK) 3) A 400-line regex to parse YAML (yes, regex for YAML) 4) An exec('diff') because PHP's array_diff was 'too slow' 5) Error handling: if($error) die($error); It's 2,000 lines. It checks translations. That's it. That's the tool. */ ?>",
  "pvz-fuzion-console-manager.highlights.0": "preg_match('/^(?:(?:(?:translation)(?:_(?:missing|found))?)(?:\\s+(?:v[0-9]+))?)$/i', $line); // regex that nobody can read or modify",
  "pvz-fuzion-console-manager.highlights.1": "exec('diff file1.json file2.json', $output); // PHP shelling out to diff. peak engineering.",
  "pvz-fuzion-console-manager.highlights.2": "$community = mysql_num_rows(mysql_query('SELECT * FROM contributors')); // counting contributors via full table scan",
  "pvz-fuzion-console-manager.lessonsLearned.0": "// Learned: don't parse YAML with regex. (still doing it though)",
  "pvz-fuzion-console-manager.lessonsLearned.1": "// Learned: exec('diff') is not a long-term strategy",
  "pvz-fuzion-console-manager.lessonsLearned.2": "// Learned: 'simple utility script' is the origin story of every monolith",
  "shreksophone.shortDescription": "<?php header('Content-Type: audio/shrek'); // not a real MIME type. doesn't matter. it's Shrek. ?>",
  "shreksophone.longDescription": "<?php /* Shreksophone. The magnum opus. The pinnacle of web development. One PHP file: shrek.php. It contains: 1) An <embed> tag pointing to shrek_sax.mp3 (autoplay, loop, hidden=false) 2) A <body bgcolor='#00FF00'> because Shrek is green 3) document.body.innerHTML='<video src=shrek.mp4 autoplay loop style=position:fixed;top:0;left:0;width:100vw;height:100vh>'; 4) A visitor counter using a flat file: $count = file_get_contents('counter.txt'); file_put_contents('counter.txt', $count+1); 5) register_globals is ON. The video source IS a global variable. Anyone can change the video via URL. This is by design. This is art. */ ?>",
  "shreksophone.highlights.0": "echo '<body bgcolor=\"#00FF00\" text=\"#FFFFFF\">'; // web design at its peak",
  "shreksophone.highlights.1": "echo '<embed src=\"shrek.mid\" autostart=\"true\" loop=\"true\">'; // MIDI autoplay. no consent needed.",
  "shreksophone.highlights.2": "$_GET['creative_direction'] = 'yes'; // creative direction: parameter-driven",
  "shreksophone.lessonsLearned.0": "// Learned: absurdity IS a valid design pattern",
  "shreksophone.lessonsLearned.1": "// Learned: 1 PHP file can have more impact than 10,000 lines of enterprise code",
  "shreksophone.lessonsLearned.2": "// Learned: <embed autostart=true> sparks joy and lawsuits",
  "glossairequest.shortDescription": "mysql_query('SELECT question FROM quiz ORDER BY RAND() LIMIT 10'); // random quiz, worst performance possible",
  "glossairequest.longDescription": "<?php /* GlossaireQuest: a quiz app. The authentication: md5($_POST['password']). No salt. The sessions: session_start() on every page, including the 404 page. The quiz engine: a 600-line switch statement. case 'question1': case 'question2': etc. Adding a question means adding a case. There are 847 cases. The scoring: stored in a cookie. Client-side. Users can edit their score. Some did. The leaderboard is compromised. We call it 'gamification'. The admin panel: /admin.php (no auth, just an obscure URL). Someone found it. */ ?>",
  "glossairequest.highlights.0": "echo md5($password); // 'JWT? We have md5. Same thing.' - me in 2016",
  "glossairequest.highlights.1": "setcookie('score', $score); // client-side score storage. gamers love this one trick.",
  "glossairequest.highlights.2": "include('front.php'); include('back.php'); // separation of concerns achieved",
  "glossairequest.lessonsLearned.0": "// Learned: md5 is not encryption. it's barely hashing. it's a cry for help.",
  "glossairequest.lessonsLearned.1": "// Learned: /admin.php without auth is 'security through obscurity' and it doesn't work",
  "glossairequest.lessonsLearned.2": "// Learned: switch statements with 847 cases need therapy, not maintenance",
  "league-of-data-base.shortDescription": "mysql_query('SELECT * FROM champions, items, runes'); // cartesian product of the entire game. 47 million rows.",
  "league-of-data-base.longDescription": "<?php /* League of Data Base: a name that's also a SQL joke. The app fetches champion data via file_get_contents('https://ddragon.leagueoflegends.com/...'). No caching. Every. Single. Page load. Riot's API rate limit hit us on day 2. Solution: sleep(1) between requests. The images are stored locally via copy(). The storage optimization: hard links (actually a good idea, credit where credit is due). The i18n: a 2000-line array mapping champion names. $champions['Aatrox']['fr'] = 'Aatrox'; Yes, most names are the same in French. We mapped them all anyway. */ ?>",
  "league-of-data-base.highlights.0": "file_get_contents($riot_api); // no caching, no rate limiting, no mercy",
  "league-of-data-base.highlights.1": "link('image_original.jpg', 'image_copy.jpg'); // hard links: the one actually good idea",
  "league-of-data-base.highlights.2": "echo '<table border=1>'; // responsive: no. fast: also no. table: yes.",
  "league-of-data-base.lessonsLearned.0": "// Learned: APIs have rate limits. sleep(1) is not caching.",
  "league-of-data-base.lessonsLearned.1": "// Learned: hard links are underrated. also, disk space is finite.",
  "league-of-data-base.lessonsLearned.2": "// Learned: mapping identical translations is peak busywork",
  "blender-collection.shortDescription": "<?php $blender = new stdClass(); $blender->addons = array(); // OOP without the OO ?>",
  "blender-collection.longDescription": "<?php /* Blender Collection: a platform to manage Blender add-ons. Users create collections, toggle visibility, and download everything as a zip. The zip: exec('zip -r addons.zip /tmp/addons/'); No sanitization of filenames. One user named their addon '../../etc/passwd'. The download didn't work but we learned about path traversal that day. The admin dashboard: SELECT COUNT(*) for analytics. Queries run on page load. The dashboard takes 30 seconds to render. We added a loading spinner. Problem solved. The worker queue: a while(true) loop in a PHP script started via nohup. It's been running for 8 months. PID unknown. */ ?>",
  "blender-collection.highlights.0": "if($_SESSION['role'] == 'admin') { // role check via string comparison in session",
  "blender-collection.highlights.1": "mysql_query('SELECT COUNT(*) FROM everything'); // analytics: count everything, always",
  "blender-collection.highlights.2": "while(true) { processQueue(); sleep(5); } // the immortal worker. nohup'd into eternity.",
  "blender-collection.lessonsLearned.0": "// Learned: sanitize filenames or learn about path traversal the hard way",
  "blender-collection.lessonsLearned.1": "// Learned: SELECT COUNT(*) on every page load is not 'real-time analytics'",
  "blender-collection.lessonsLearned.2": "// Learned: nohup php worker.php & is not a deployment strategy",
  "symfony-session.shortDescription": "<?php session_start(); session_start(); // called twice. the second one warns. we suppress it. ?>",
  "symfony-session.longDescription": "<?php /* Symfony Session: ironic name because the early version had zero Symfony and was pure session abuse. Version 1: Everything in $_SESSION. User data? Session. Auth? Session. Shopping cart? Session (we didn't have a shop). Admin state? Session. The session file was 4MB per user. The /tmp directory filled up weekly. Cron job to delete sessions every Sunday night. Users logged in Monday morning to find their data gone. 'It's a feature: weekly fresh start.' We eventually migrated to actual Symfony. The sessions are still 4MB. Some habits die hard. */ ?>",
  "symfony-session.highlights.0": "<?php /* Captcha: we used a PHP script that generated an image with imagecreatetruecolor(). The font was Comic Sans. The distortion was random imagerotate(). Bots solved it faster than humans. */ ?>",
  "symfony-session.highlights.1": "mysql_query('INSERT INTO stagiaires VALUES (...)'); // CRUD with no field names. column order is destiny.",
  "symfony-session.highlights.2": "<?php require('fpdf.php'); $pdf->Cell(0,10,'Calendar',1); // PDF gen via FPDF. each cell manually positioned. 800 lines for a timetable. ?>",
  "symfony-session.lessonsLearned.0": "// Learned: sessions are not a database",
  "symfony-session.lessonsLearned.1": "// Learned: MVC means Model View Controller, not Massive Verbose Code",
  "symfony-session.lessonsLearned.2": "// Learned: clearing /tmp weekly is not a data retention policy",
  "timeline.featured": "echo '<blink><font color=red>PROJET CLÉ</font></blink>'; // featured: blink it",
  "timeline.detail": "echo '<a href=\"detail.php?id=' . $id . '\">Voir</a>'; // the classic unsanitized link",
  "timeline.aria": "<!-- timeline: it's a table. screen readers love tables. right? -->",
  "modal.close": "echo '</div></div></div></div></div>'; // close ALL the divs",
  "modal.image_fullscreen": "echo '<img src=\"' . $img . '\" width=100% height=100%>'; // distorted but FULL screen",
  "modal.previous": "echo '<a href=\"?id=' . ($id-1) . '\">Précédente</a>'; // no bounds check. id=-1 shows... something",
  "modal.next": "echo '<a href=\"?id=' . ($id+1) . '\">Suivante</a>'; // id=99999 shows a blank page. or an error. depends.",
  "modal.video_title": "echo '<embed src=\"video.avi\">'; // embed an AVI. in 2026. legacy never dies.",
  "modal.description": "echo nl2br($description); // newlines to <br>: the PHP developer's markdown",
  "modal.lessons": "echo '<ol>'; while($row = mysql_fetch_array($lessons)) { echo '<li>' . $row[0]; } // no closing </li>. no closing </ol>. HTML is forgiving.",
  "modal.highlights": "echo '<ul>' . implode('', array_map(function($h) { return '<li>'.$h; }, $highlights)); // PHP 5.3 closures in a PHP 4 codebase. progress.",
  "modal.links": "echo '<a href=\"' . $url . '\" target=_blank>'; // target=_blank without rel=noopener. phishing vector: open.",
  "modal.demo": "header('Location: demo.php'); // redirect to demo. demo.php is a 404. has been for 3 years.",
  "modal.site": "echo '<iframe src=\"' . $site . '\" width=100% height=600>'; // iframe: the original micro-frontend",
  "today": "echo date('Y-m-d'); // at least date() works without mysql_connect"
}
