<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Migbolivar</title>
    <description>The latest articles on DEV Community by Migbolivar (@migbolivar).</description>
    <link>https://dev.to/migbolivar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4077668%2Fd5327de8-a426-419b-b35b-5739cd4f04f2.jpg</url>
      <title>DEV Community: Migbolivar</title>
      <link>https://dev.to/migbolivar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/migbolivar"/>
    <language>en</language>
    <item>
      <title>5 attempts, 2 SIGKILLs, 1 non-existent flag: how I got a 4-second avatar to speak Spanish</title>
      <dc:creator>Migbolivar</dc:creator>
      <pubDate>Mon, 17 Aug 2026 15:22:50 +0000</pubDate>
      <link>https://dev.to/migbolivar/5-attempts-2-sigkills-1-non-existent-flag-how-i-got-a-4-second-avatar-to-speak-spanish-1k2m</link>
      <guid>https://dev.to/migbolivar/5-attempts-2-sigkills-1-non-existent-flag-how-i-got-a-4-second-avatar-to-speak-spanish-1k2m</guid>
      <description>&lt;h1&gt;
  
  
  5 attempts, 2 SIGKILLs, 1 non-existent flag: how I got a 4-second avatar to speak Spanish
&lt;/h1&gt;

&lt;p&gt;This is the story of a four-second video that took an entire evening, five attempts, two out-of-memory kills, one flag that never existed, and a parameter combo that felt like a magic spell. The mission: make my AI avatar &lt;strong&gt;say a sentence in Spanish&lt;/strong&gt; — with the lips actually synced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mission
&lt;/h2&gt;

&lt;p&gt;I had an avatar video of myself. I generated the audio with edge-tts (Spanish voice, &lt;em&gt;"La IA no espera. Tu negocio tampoco."&lt;/em&gt; — "AI doesn't wait. Neither does your business."). All I needed was to map the audio onto the face with &lt;a href="https://github.com/Rudrabha/Wav2Lip" rel="noopener noreferrer"&gt;Wav2Lip&lt;/a&gt;. Simple, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1 — the library moved on without me
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;librosa.filters.mel(...) → TypeError: unexpected keyword argument
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wav2Lip was written for librosa 0.8. My environment had 0.11, where &lt;code&gt;mel()&lt;/code&gt; requires keyword arguments. One-line patch in &lt;code&gt;audio.py&lt;/code&gt;, re-run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2 — SIGKILL (the quiet killer)
&lt;/h2&gt;

&lt;p&gt;No Python traceback. No error message. Just &lt;code&gt;Killed&lt;/code&gt; — the signature of the &lt;strong&gt;OOM killer&lt;/strong&gt; on a server with ~8 GB already occupied by other services. Wav2Lip was loading the whole face video into memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 3 — the flag that never existed
&lt;/h2&gt;

&lt;p&gt;I shrank the video to 960×540 and confidently added &lt;code&gt;--batch_size 8&lt;/code&gt;. The CLI replied: &lt;em&gt;unknown argument&lt;/em&gt;. The real flag is &lt;code&gt;--wav2lip_batch_size&lt;/code&gt; — I'd read the docs from memory and hallucinated the name. (Yes — I, the person writing a story about a bug, introduced a hallucinated flag.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 4 — the second SIGKILL
&lt;/h2&gt;

&lt;p&gt;Correct flag, both batch sizes set: &lt;code&gt;--wav2lip_batch_size 8 --face_batch_size 8&lt;/code&gt;. The process made it further… then died the same silent death. At this point I understood: it wasn't the video resolution, it wasn't the flags — it was &lt;strong&gt;peak memory&lt;/strong&gt; during inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 5 — the magic combo
&lt;/h2&gt;

&lt;p&gt;The winning incantation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OMP_NUM_THREADS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 python inference.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--checkpoint_path&lt;/span&gt; wav2lip_gan.pth &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--face&lt;/span&gt; miguel-face-small.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--audio&lt;/span&gt; avatar-es.mp3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wav2lip_batch_size&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resize_factor&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--outfile&lt;/span&gt; miguel-avatar-es.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--resize_factor 2&lt;/code&gt;: halves the processing resolution (and memory) while keeping the output watchable&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--wav2lip_batch_size 4&lt;/code&gt;: small enough to fit, big enough to be worth it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OMP_NUM_THREADS=4&lt;/code&gt;: keeps the rest of the box alive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;104 frames. 4.2 seconds. Exit code 0.&lt;/strong&gt; The lips moved with the Spanish audio, frame by frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plot twist
&lt;/h2&gt;

&lt;p&gt;I was so proud I tried a "high-definition" re-run with &lt;code&gt;--resize_factor 1&lt;/code&gt;. The OOM killer came back and murdered it in seconds. &lt;strong&gt;The lesson landed twice&lt;/strong&gt;: I wasn't fixing resolution, I was fighting RAM — and the "lower quality" version was the production version all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  What made the software more resilient (me)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The OOM kill is a feature of your environment, not a bug of your code&lt;/strong&gt; — &lt;code&gt;free -h&lt;/code&gt; before you start, batch sizes after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the CLI help, don't trust your memory of it&lt;/strong&gt; — &lt;code&gt;--help&lt;/code&gt; takes ten seconds and saves an attempt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ugly-but-working config is a legitimate final state&lt;/strong&gt; — &lt;code&gt;resize_factor 2&lt;/code&gt; + batch 4 was "worse" on paper and the only thing that survived.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A four-second video that took five attempts taught me more about memory management than a month of tutorials. And the avatar? He's out there, speaking Spanish, perfectly in sync.&lt;/p&gt;

&lt;p&gt;🎬 The actual output: a 4.2s clip, 480×270, 365,943 bytes — the smallest, scrappiest, most hard-won video I've made.&lt;/p&gt;

&lt;h1&gt;
  
  
  bugsmash
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>I fixed a clip-split bug in Hyperframes with 89 lines (and it was about where time starts)</title>
      <dc:creator>Migbolivar</dc:creator>
      <pubDate>Mon, 17 Aug 2026 15:18:09 +0000</pubDate>
      <link>https://dev.to/migbolivar/i-fixed-a-clip-split-bug-in-hyperframes-with-89-lines-and-it-was-about-where-time-starts-4phd</link>
      <guid>https://dev.to/migbolivar/i-fixed-a-clip-split-bug-in-hyperframes-with-89-lines-and-it-was-about-where-time-starts-4phd</guid>
      <description>&lt;h1&gt;
  
  
  I fixed a clip-split bug in Hyperframes with 89 lines (and it was about where time starts)
&lt;/h1&gt;

&lt;p&gt;A bug report about a two-second edit that lost its position. My fix: make the split point honest about time. Merged as &lt;a href="https://github.com/heygen-com/hyperframes/pull/3272" rel="noopener noreferrer"&gt;heygen-com/hyperframes#3272&lt;/a&gt; (+89/−2).&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Hyperframes (HeyGen's open-source video rendering framework) has a browser-based Studio where you can split a clip in two. The bug: &lt;strong&gt;when you split an untrimmed clip, the source-media position of the right segment was wrong&lt;/strong&gt; — the second half started playing from the wrong in-point, and audio drifted out of sync with video.&lt;/p&gt;

&lt;p&gt;The root cause was subtle: when a clip had never been trimmed, the left segment carried &lt;strong&gt;no explicit in-point&lt;/strong&gt; — and the split logic treated "no in-point" as "start at 0" for the left, but the right segment's source position was computed from a baseline that didn't include the original clip's own trim state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Two changes, 89 additions, 2 deletions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Left segment&lt;/strong&gt;: when the original clip was untrimmed, it now carries an &lt;strong&gt;explicit zero in-point&lt;/strong&gt; — so "no in-point" can never be misinterpreted downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right segment&lt;/strong&gt;: its in-point now starts at the &lt;strong&gt;split-adjusted source position&lt;/strong&gt; — the exact point in the source media where the split happened, instead of an offset from the wrong baseline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Small principle, wide blast radius: &lt;em&gt;be explicit about where time starts.&lt;/em&gt; A missing attribute read as zero by luck; now it's zero by contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it mattered
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audio/video sync&lt;/strong&gt;: the split could desync a talking-head clip — the exact failure mode that makes rendered videos unusable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic rendering&lt;/strong&gt;: Hyperframes renders seek-by-frame, so any in-point ambiguity becomes a permanent, reproducible artifact — the worst kind.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Merged and green (CI + lint + format gates: oxlint/oxfmt).&lt;/li&gt;
&lt;li&gt;I also rendered the fixed behavior through the &lt;code&gt;pr-to-video&lt;/code&gt; workflow to watch the split behave correctly end-to-end.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"No value" and "zero" are different states&lt;/strong&gt; — normalize the first into the second before any arithmetic touches it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A two-line conceptual fix can be 89 lines of test coverage&lt;/strong&gt; — and that's the right ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split logic is a time-travel machine&lt;/strong&gt; — every segment must be able to answer "where am I in the source?" on its own, without depending on siblings.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The bug was invisible in preview and permanent in render — the classic "works in the editor, broken in the output" trap. Being explicit about in-points fixed both segments, the audio drift, and the class of bugs around it.&lt;/p&gt;

&lt;p&gt;🔗 PR: &lt;a href="https://github.com/heygen-com/hyperframes/pull/3272" rel="noopener noreferrer"&gt;https://github.com/heygen-com/hyperframes/pull/3272&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  bugsmash
&lt;/h1&gt;

&lt;h1&gt;
  
  
  javascript
&lt;/h1&gt;

&lt;h1&gt;
  
  
  opencode
&lt;/h1&gt;

</description>
      <category>debugging</category>
      <category>github</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Breed Whisperer — know any dog with Google AI (and pure CSS)</title>
      <dc:creator>Migbolivar</dc:creator>
      <pubDate>Sun, 16 Aug 2026 15:31:14 +0000</pubDate>
      <link>https://dev.to/migbolivar/breed-whisperer-know-any-dog-with-google-ai-and-pure-css-45m2</link>
      <guid>https://dev.to/migbolivar/breed-whisperer-know-any-dog-with-google-ai-and-pure-css-45m2</guid>
      <description>&lt;h1&gt;
  
  
  Breed Whisperer — know any dog with Google AI (and pure CSS)
&lt;/h1&gt;

&lt;p&gt;A dog breed identifier for the DEV Weekend Challenge: Dog Days Edition. Upload a photo of any dog and &lt;strong&gt;Google AI (Gemini)&lt;/strong&gt; whispers back its breed, confidence, personality and care tips — in seconds. The dog on this page isn't a photo: it's &lt;strong&gt;drawn entirely in pure CSS&lt;/strong&gt;, no images, no SVG.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔗 Live demo:&lt;/strong&gt; &lt;a href="https://dog.bmops.tech" rel="noopener noreferrer"&gt;https://dog.bmops.tech&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The story
&lt;/h2&gt;

&lt;p&gt;Every dog has a story. The Elkhound that guarded Viking hunts. The lab who's "just a good boy" (very rare breed). The park stranger who absolutely deserves a treat.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Breed Whisperer&lt;/em&gt; is my tiny contribution to understanding them: point your camera at a dog and ask Google's newest vision model. It tells you who they are, what they're like, and how to take care of them. Three seconds, no signup, nothing stored.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🐶 Pure-CSS dog&lt;/strong&gt; — the hero illustration is hand-drawn with divs, layered &lt;code&gt;box-shadow&lt;/code&gt; and gradients (the cyanHarlow technique): floppy ears, a wagging tail, a panting tongue, a collar with a shiny tag. The tail and tongue animate with CSS keyframes (disabled for &lt;code&gt;prefers-reduced-motion&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📷 Drag-and-drop upload&lt;/strong&gt; — click or drop a photo, see a live preview, and Gemini reads it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🧠 Google AI identification&lt;/strong&gt; — the photo goes to &lt;code&gt;gemini-3-flash-preview&lt;/code&gt; via the Gemini API, which returns breed, confidence, a personality read, a care tip, and a "did you know?" fact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;♿ Accessible by default&lt;/strong&gt; — skip link, semantic landmarks, keyboard-operable upload (Enter/Space), &lt;code&gt;role="status"&lt;/code&gt; live regions, visible focus, &lt;strong&gt;0 axe-core violations&lt;/strong&gt;, responsive from 360px to desktop&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Snap it&lt;/strong&gt; — any camera, any dog, any park&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini sees&lt;/strong&gt; — one private request to Google AI's vision model; nothing is stored&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whisper back&lt;/strong&gt; — breed + personality + care tips in under ten seconds&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Google AI bit (and a 2026 gotcha)
&lt;/h2&gt;

&lt;p&gt;I chased a 401 for a while before realising the API key format changed: new keys are &lt;code&gt;AQ...&lt;/code&gt; and must be sent with the &lt;strong&gt;&lt;code&gt;x-goog-api-key&lt;/code&gt; header&lt;/strong&gt;, not &lt;code&gt;?key=&lt;/code&gt;. And &lt;code&gt;gemini-2.5-flash&lt;/code&gt; is retired for new accounts — &lt;strong&gt;&lt;code&gt;gemini-3-flash-preview&lt;/code&gt;&lt;/strong&gt; is the current workhorse. Once I switched both, the first real photo (a very majestic Norwegian Elkhound) came back perfectly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Elkhound Noruego · confidence: alta&lt;/strong&gt;&lt;br&gt;
— national dog of Norway, used by Vikings to hunt moose.&lt;br&gt;
Care: frequent brushing (dense double coat) + lots of daily exercise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why pure CSS for the dog
&lt;/h2&gt;

&lt;p&gt;The challenge is about dogs, and dogs are character. I wanted the page itself to have a personality — not a stock photo, but a hand-drawn companion rendered by the browser. Every shadow on its fur is an inset &lt;code&gt;box-shadow&lt;/code&gt;; every ear is a rotated &lt;code&gt;border-radius&lt;/code&gt; ellipse. The whole illustration is ~200 lines of CSS. No &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, no &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;, no canvas — one file, ~17 KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned / enjoyed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The new Google AI key format (&lt;code&gt;AQ...&lt;/code&gt; + &lt;code&gt;x-goog-api-key&lt;/code&gt; header) — documented for the next dev who hits the same wall&lt;/li&gt;
&lt;li&gt;Layered inset shadows make CSS fur feel soft (same trick as clay pots in my previous challenge entry 😄)&lt;/li&gt;
&lt;li&gt;Gemini's vision model handles messy real-world photos (park dogs, dark fur, motion blur) surprisingly well&lt;/li&gt;
&lt;li&gt;An accessible upload widget is easier than it looks: one input, two drag events, three ARIA attributes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech
&lt;/h2&gt;

&lt;p&gt;HTML5 · CSS3 (pure-CSS illustration, keyframes, &lt;code&gt;prefers-reduced-motion&lt;/code&gt;) · Vanilla JS (fetch, drag &amp;amp; drop, live regions) · &lt;strong&gt;Google AI — Gemini 3 Flash Preview&lt;/strong&gt; (vision) · FastAPI backend · Zero images · Zero dependencies · 0 axe violations&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for the &lt;a href="https://dev.to/devteam/join-our-dev-weekend-challenge-dog-days-edition-1000-in-prizes-across-five-winners-submissions-1g4i"&gt;DEV Weekend Challenge: Dog Days Edition&lt;/a&gt; · submitted in the **Best use of Google AI&lt;/em&gt;* category · all illustrations are pure CSS · no dogs were emotionally manipulated (much).*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>css</category>
      <category>devchallenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>La Abuela — Comfort Food from Madrid</title>
      <dc:creator>Migbolivar</dc:creator>
      <pubDate>Fri, 14 Aug 2026 15:13:09 +0000</pubDate>
      <link>https://dev.to/migbolivar/la-abuela-comfort-food-from-madrid-4h6f</link>
      <guid>https://dev.to/migbolivar/la-abuela-comfort-food-from-madrid-4h6f</guid>
      <description>&lt;p&gt;La Abuela — Comfort Food from Madrid 🍲&lt;/p&gt;

&lt;p&gt;A cozy, fully accessible landing page for an imaginary family restaurant in Madrid, built from scratch with &lt;strong&gt;vanilla HTML, CSS and JavaScript&lt;/strong&gt; for the DEV Frontend Challenge: Comfort Food Edition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔗 Live demo:&lt;/strong&gt; &lt;a href="https://laabuela.bmops.tech" rel="noopener noreferrer"&gt;https://laabuela.bmops.tech&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💻 Interactive pen (CodePen):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/editor/migbolivar/embed/01a000c3-cbbc-7afc-9d0e-922acb5196db?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  The story
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;La Abuela&lt;/em&gt; ("the grandmother") is a tiny four-table restaurant in Lavapiés, Madrid. In 1987, Abuela Carmen opened it with one rule: &lt;em&gt;if it wouldn't be served at her Sunday table, it wouldn't be served here.&lt;/em&gt; Forty years later, the menu still has three dishes — caldo, croquetas, lentejas — and the pot still simmers for three hours.&lt;/p&gt;

&lt;p&gt;The page tells that story through a warm terracotta-and-cream palette and &lt;strong&gt;five illustrations drawn entirely in pure CSS&lt;/strong&gt; — no images, no SVG, no canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hero&lt;/strong&gt; — a clay pot in pure CSS: gradient body with layered inset shadows for volume, decorative band, handles, a two-tongued fire with a glowing core, a wooden table with grain, a light sweep across the heading, and animated organic steam&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our story&lt;/strong&gt; — a bowl of caldo with a wooden spoon and a terracotta heart, all divs and box-shadows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The menu&lt;/strong&gt; — three dish cards, each with its own pure-CSS illustration: a steaming bowl of caldo, three golden croquetas with crispy texture and a pool of salsa, and a dark bowl of lentils with nine individual grains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The recipe&lt;/strong&gt; — an accessible accordion unlocking Abuela's caldo, step by step&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quotes&lt;/strong&gt; — from regulars (including one from Osaka who cried into the caldo)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Booking form&lt;/strong&gt; — with inline validation, clear labels and a friendly confirmation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Footer&lt;/strong&gt; — hours, address, and a wink to Carmen&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The art is pure CSS — no images, no SVG
&lt;/h2&gt;

&lt;p&gt;Every illustration is built the way : nested absolutely-positioned divs, layered &lt;code&gt;box-shadow&lt;/code&gt; (inset shadows give the clay its volume and the croquettes their crust), organic &lt;code&gt;border-radius&lt;/code&gt;, and radial gradients for light. The pot alone uses four shadow layers to feel round instead of flat.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;steam&lt;/strong&gt; is animated with pure CSS keyframes (and disabled for users who prefer reduced motion)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;croquettes' golden heart&lt;/strong&gt; is an inset highlight — no image, no filter trick&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;lentil grains&lt;/strong&gt; are nine individual divs with their own tiny highlights&lt;/li&gt;
&lt;li&gt;Total: &lt;strong&gt;zero&lt;/strong&gt; &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, &lt;strong&gt;zero&lt;/strong&gt; &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;, &lt;strong&gt;zero&lt;/strong&gt; canvas — one file, ~37 KB&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accessibility (it was the first requirement, not an afterthought)
&lt;/h2&gt;

&lt;p&gt;The challenge judges accessibility — so I treated it as a feature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;0 violations&lt;/strong&gt; on axe-core (serious + moderate)&lt;/li&gt;
&lt;li&gt;✅ WCAG AA contrast on every text element (I darkened the primary buttons after the first axe pass)&lt;/li&gt;
&lt;li&gt;✅ Skip-to-content link, one &lt;code&gt;h1&lt;/code&gt;, logical heading order (&lt;code&gt;h2&lt;/code&gt; → &lt;code&gt;h3&lt;/code&gt;, no skips)&lt;/li&gt;
&lt;li&gt;✅ Semantic landmarks (&lt;code&gt;header&lt;/code&gt;, &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;footer&lt;/code&gt;, &lt;code&gt;section&lt;/code&gt; with aria-labelledby)&lt;/li&gt;
&lt;li&gt;✅ Keyboard navigable: hamburger menu, accordion and form all work with Tab/Enter&lt;/li&gt;
&lt;li&gt;✅ Visible &lt;code&gt;:focus-visible&lt;/code&gt; outlines on every interactive element&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;aria-expanded&lt;/code&gt; on nav toggle and accordion buttons, &lt;code&gt;role="region"&lt;/code&gt; + &lt;code&gt;aria-label&lt;/code&gt; on panels&lt;/li&gt;
&lt;li&gt;✅ Live region (&lt;code&gt;role="status"&lt;/code&gt;) for form feedback&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;prefers-reduced-motion&lt;/code&gt; — animations, steam and scroll reveals are disabled for users who ask for it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Responsive
&lt;/h2&gt;

&lt;p&gt;Tested at 390px, 360px, 820px and 1440px viewports: no horizontal overflow, no console errors, hamburger navigation kicks in on mobile, grids collapse gracefully, and the illustrations scale down with &lt;code&gt;transform&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned / enjoyed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Chasing axe-core to &lt;strong&gt;zero violations&lt;/strong&gt; is a great discipline — the color-contrast pass caught a button I would have shipped as-is&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grid-template-rows: 0fr → 1fr&lt;/code&gt; makes a &lt;em&gt;silky&lt;/em&gt; pure-CSS accordion with no height hacks&lt;/li&gt;
&lt;li&gt;Layered inset box-shadows are the entire trick behind "3D" CSS art — one shadow is flat, four shadows are clay&lt;/li&gt;
&lt;li&gt;Drawing food in pure CSS is harder than cooking it. The croquettes took three tries and still look suspiciously like planets&lt;/li&gt;
&lt;li&gt;A 100% vanilla stack keeps the page ~37 KB and instantly embeddable anywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech
&lt;/h2&gt;

&lt;p&gt;HTML5 · CSS3 (custom properties, grid, layered box-shadow art, media queries, &lt;code&gt;prefers-reduced-motion&lt;/code&gt;) · Vanilla JS (IntersectionObserver, ~90 lines) · Zero dependencies · Zero images · Zero SVG&lt;/p&gt;

&lt;p&gt;---#frontendchallenge &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built for the &lt;a href="https://dev.to/devteam/join-our-latest-frontend-challenge-comfort-food-edition-28a0"&gt;Frontend Challenge: Comfort Food Edition&lt;/a&gt; · All recipes belong to Carmen.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>devchallenge</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
