<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Ravi Ranjan - Blog</title>
        <link>https://ravi-ranjan.in</link>
        <description>Technical articles and tutorials on full-stack development, React, Next.js, Node.js, and modern web technologies.</description>
        <lastBuildDate>Tue, 22 Sep 2026 18:46:46 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Ravi Ranjan - Blog</title>
            <url>https://ravi-ranjan.in/favicon.ico</url>
            <link>https://ravi-ranjan.in</link>
        </image>
        <copyright>All rights reserved 2026, Ravi Ranjan</copyright>
        <item>
            <title><![CDATA[Learning CSS Grid the Hard Way]]></title>
            <link>https://ravi-ranjan.in/articles/learning-css-grid-the-hard-way</link>
            <guid isPermaLink="false">https://ravi-ranjan.in/articles/learning-css-grid-the-hard-way</guid>
            <pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I'd never written a line of CSS Grid. A challenge made me build a layout without flexbox, I got stuck copying things I didn't understand, so I stopped and worked out how grid actually thinks.]]></description>
            <content:encoded><![CDATA[<h2>Everything I Built, I Built With Flexbox</h2>
<p>I've built everything I've built with flexbox. Navs, cards, forms, whole page shells. It stretched to cover all of it, and because it kept working I kept not learning grid. It sat in the same folder in my head as service workers: real, useful, worth understanding one day.</p>
<p>Then I picked up a challenge that took the option away. Build a photo gallery, <strong>no flexbox for layout.</strong> Grid only. I got a fair way in by copying shapes from the brief, and then I hit the wall you hit when you're copying things you don't understand. The layout mostly worked. I couldn't have told you why. And the moment something looked off, I had no idea which property was lying to me.</p>
<p>So I stopped building and wrote this instead. It's not a tutorial, it's the notes I needed before I could go back and finish that gallery knowing what I was doing. If you're coming from flexbox, I'd guess the same things will trip you.</p>
<p>The assumption underneath all of it, the one I didn't know I was making: <strong>grid is flexbox with a second axis.</strong> It isn't. The two disagree about something basic, which is who gets to decide how big things are. Almost everything that confused me comes back to that. Every diagram below is a real grid, so move the controls and watch the CSS change.</p>
<hr />
<h2>Content-Out vs Layout-In</h2>
<p>Here's the model I replaced it with, and I'm putting it first because the rest of this article is just it showing up in different places: <strong>flexbox is content-out, grid is layout-in.</strong></p>
<p>In flexbox you describe how items should <em>behave</em> and let the content settle the result. You say &quot;grow, shrink, don't go below your basis,&quot; and the text inside each item decides the actual widths. In grid you define the <em>structure</em> first, a set of tracks that exist whether or not anything is in them, and then you put items into it.</p>
<p>This is also why the usual advice never landed for me. Everyone says flexbox is one-dimensional and grid is two-dimensional, and that falls apart fast. Plenty of single-row layouts want grid. Plenty of wrapping flex rows look two-dimensional. The question that actually seems to decide it is <strong>who should control the size here, the content or the container?</strong></p>
<table><thead><tr><th>Situation</th><th>Reach for</th><th>Because</th></tr></thead><tbody><tr><td>Nav bar, tag list, button row</td><td>flexbox</td><td>item widths come from their own text</td></tr><tr><td>Card gallery, image mosaic</td><td>grid</td><td>tracks should be uniform regardless of content</td></tr><tr><td>Page shell (header/sidebar/main)</td><td>grid</td><td>named areas, and rows relate to columns</td></tr><tr><td>Centering one thing</td><td>grid</td><td>place-items: center</td></tr><tr><td>Inside a card (title, body, button)</td><td>flexbox</td><td>one direction, content-driven</td></tr><tr><td>Aligning across sibling cards</td><td>grid + subgrid</td><td>flexbox cannot see into siblings</td></tr></tbody></table>
<p>They nest fine, and most real pages use both. Grid for the page skeleton and the gallery, flexbox inside each card. That's not a compromise, it's how they're meant to split the work. Hang on to content-out versus layout-in, because the next eight sections are all the same idea from different angles.</p>
<h2>There Are Always Two Grids</h2>
<p>The first thing that follows from layout-in thinking took me a few confused minutes to accept: <strong>there are always two grids</strong>, and the second one gets made for you.</p>
<p>The explicit grid is the one I write down, the tracks in <code>grid-template-columns</code> and <code>grid-template-rows</code>. The implicit grid is what the browser adds when there are more items than declared tracks. It makes rows on demand, and those rows are sized by <code>grid-auto-rows</code>, not by my template.</p>
<pre><code class="language-css">.grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;  /* explicit: 3 columns */
  grid-auto-rows: 80px;                /* implicit: every invented row */
}</code></pre>
<p>Give that nine items and you get three neat rows. Give it ten and the browser quietly makes a fourth row you never asked for. That's not a bug, that's the implicit grid doing its job. The reason it's worth knowing early: a row sized by <code>grid-auto-rows</code> ignores anything you write in <code>grid-template-rows</code>. So if a row is the wrong height and editing the template does nothing, you're looking at the other grid. Add items in the demo and watch the dashed rows show up.</p>

<hr />
<h2>1fr Is Not flex: 1</h2>
<p>A track is a column or a row. You can size tracks with any normal unit, but grid adds one of its own: <code>fr</code>, a share of the <em>leftover</em> space. That word matters more than it looks. The browser lays out the fixed tracks first, takes off the gaps, and only then splits what's left. So <code>200px 1fr 1fr</code> means &quot;200 pixels, then split the rest in half.&quot; The two <code>1fr</code> tracks match each other, but neither has any fixed relationship to the 200px.</p>
<p>Coming from flexbox this looks exactly like <code>flex: 1</code>, and that's the trap I walked into. Here's the difference: <code>1fr</code> has a minimum size of <code>auto</code>, so the track won't shrink below whatever its content needs. One long unbroken string or a wide image puts a floor under the track, the track pushes the grid wider than its container, and the page starts scrolling sideways. The content wins, and you didn't know there was an argument.</p>
<pre><code class="language-css">grid-template-columns: 200px 1fr 1fr;      /* fixed, then split the rest */
grid-template-columns: minmax(0, 1fr) 1fr; /* the overflow fix */</code></pre>
<div class="callout callout-tip"><strong>The fix everyone reaches for</strong><p><code>minmax(0, 1fr)</code> is just 1fr with the floor set to 0, so the track <em>can</em> shrink. You'll see it all over other people's grid code, and before I understood the <code>auto</code> minimum it looked like superstition. It isn't. It's the direct fix for a horizontal scrollbar you didn't ask for.</p></div>
<h3>minmax() and repeat()</h3>
<p><code>minmax(min, max)</code> gives a track a floor and a ceiling. <code>minmax(200px, 1fr)</code> reads as: never narrower than 200px, otherwise take a share of what's left. That one function is most of what makes grid responsive without media queries, as the next section shows.</p>
<p><code>repeat(3, 1fr)</code> is shorthand for <code>1fr 1fr 1fr</code>. It takes a count, or the more interesting option, the keywords <code>auto-fill</code> and <code>auto-fit</code>, which hand the count to the browser. Then there are the content-based ones: <code>min-content</code> is about as narrow as the longest word, <code>max-content</code> is how wide it would be with no wrapping at all, and <code>auto</code> acts like <code>max-content</code> but capped by the space available. Flipping between them in the demo is what made floors and ceilings click for me. Reading the definitions hadn't.</p>

<hr />
<h2>The Bug You Can Ship for Years</h2>
<p>These two are where I expected a clear difference and found a sneaky one instead. In the docs <code>auto-fill</code> and <code>auto-fit</code> sound nearly the same, and in most examples you'll copy they behave the same. That's not a coincidence, it's how the difference stays hidden.</p>
<p>Both tell the browser to fit as many tracks as it can. They differ on exactly one thing: what happens to the tracks that end up <em>empty</em>.</p>
<ul><li><code>auto-fill</code> keeps the empty tracks. They sit there at full width, holding space.</li><li><code>auto-fit</code> collapses empty tracks to zero width, and the items that are left stretch to fill the gap.</li></ul>
<div class="callout callout-warning"><strong>Why this hides so well</strong><p>If your items always fill every track, the two are <strong>identical</strong>. You can ship the wrong one for years and never find out, right up until a category page renders with two results instead of six.</p></div>
<p>The difference only shows up when you run out of items, which is what the demo lets you force. Drop the item count below the track count and the two split apart straight away.</p>

<p><strong>So which one do you want?</strong> Use <code>auto-fit</code> for a gallery or card list that should always look full-width. Use <code>auto-fill</code> when keeping a steady column rhythm matters more than filling the row, like a dashboard where one lonely card should stay card-sized instead of stretching across the whole screen.</p>
<hr />
<h2>Lines, -1, and the Trap Under It</h2>
<p>Grid numbers the lines <em>between</em> tracks, not the tracks. Three columns means four vertical lines, numbered 1 to 4 from the left. This catches everyone once and it definitely caught me: <code>grid-column: 1 / 3</code> doesn't mean &quot;columns 1 through 3.&quot; It means &quot;line 1 to line 3,&quot; which is two columns wide.</p>
<pre><code class="language-css">grid-column: 1 / 3;      /* line 1 to line 3 = 2 tracks wide */
grid-column: 1 / span 2; /* same thing, stated as a width  */
grid-column: span 2;     /* 2 tracks wide, auto-placed      */
grid-column: 1 / -1;     /* line 1 to the LAST line: full width */</code></pre>
<p>Negative numbers count from the end, which makes <code>-1</code> really handy: <code>1 / -1</code> spans the full width no matter how many columns there are. It looks like the answer to every full-bleed problem. Then there's a case where it quietly isn't, and that one took me a while to spot.</p>

<div class="callout callout-warning"><strong>The trap</strong><p><code>-1</code> only works against the <em>explicit</em> grid. If your columns came from <code>auto-fill</code>, the browser doesn't know where the last line is yet, so <code>1 / -1</code> quietly falls back to a single column. No error, no warning. Just a full-bleed banner that's suddenly one card wide.</p></div>
<h3>Named Lines: The Repair</h3>
<p>Square brackets in a track list name a <em>line</em>, not a track. They don't add any size, they're just labels.</p>
<pre><code class="language-css">.gallery {
  grid-template-columns:
    [full-start] repeat(auto-fill, minmax(280px, 1fr)) [full-end];
}

.featured {
  grid-column: full-start / full-end;
}</code></pre>
<p>Notice where the names sit: <strong>outside</strong> the <code>repeat()</code>. However many columns auto-fill ends up making, <code>full-start</code> is still the far-left line and <code>full-end</code> is still the far-right one. You can't do that with numbers, because you don't know the count.</p>
<p>This is the line I'd already copied into my own gallery without understanding it, and it's what sent me off to write this. Wanting a full-width item inside an <code>auto-fill</code> grid is a completely normal thing to want, and <code>grid-column: 1 / -1</code>, the obvious answer, is exactly the thing that doesn't work there. Named lines aren't a style choice in that situation. They're the only way to say it.</p>
<hr />
<h2>Drawing the Layout</h2>
<p>If layout-in ever needed one piece of evidence, it's <code>grid-template-areas</code>. You draw the layout as ASCII art. Each string is a row, each word is a named cell, and the children never find out where they are.</p>
<pre><code class="language-css">.page {
  display: grid;
  grid-template-areas:
    &quot;header header&quot;
    &quot;sidebar main&quot;
    &quot;footer footer&quot;;
  grid-template-columns: 220px 1fr;
  grid-template-rows: auto 1fr auto;
}

.page &gt; header { grid-area: header; }</code></pre>
<p>Two rules that are easy to trip over. Every row string needs the same number of columns, and an area has to be a solid rectangle. An L-shape is invalid, and when it's invalid the whole declaration gets dropped <strong>silently</strong>. That's the part worth remembering, because a typo here doesn't warn you, it just leaves you with a layout that stopped existing. Use <code>.</code> for a cell you want empty on purpose.</p>
<p>The real payoff is responsive work. Redraw the map in a media query and the whole layout rearranges without touching a single child rule:</p>
<pre><code class="language-css">@media (max-width: 700px) {
  .page {
    grid-template-areas: &quot;header&quot; &quot;main&quot; &quot;sidebar&quot; &quot;footer&quot;;
    grid-template-columns: 1fr;
  }
}</code></pre>

<hr />
<h2>dense Packing and Its Hidden Cost</h2>
<p>Auto-placement walks items in DOM order, filling row by row. When a spanning item doesn't fit in what's left at the end of a row, the browser skips ahead and leaves a hole. <code>grid-auto-flow: dense</code> changes that: if a later, smaller item would fit in an earlier hole, it gets pulled back to fill it. The gaps disappear and the grid looks tidy.</p>
<p>This is the one I'd have switched on without thinking, because it reads like a free win. The cost is real and easy to miss: visual order stops matching DOM order. Keyboard focus follows the DOM, so someone tabbing through watches focus jump around the screen, and a screen reader reads out an order that doesn't match what's on screen. Nothing in the CSS warns you. It looks perfect right up until someone tabs through it.</p>
<div class="callout callout-error"><strong>Where it is safe</strong><p>Use <code>dense</code> for decorative galleries where the order doesn't mean anything. Never for anything ordered: search results, a numbered list, a feed, steps in a process.</p></div>
<p>Turn on the DOM order numbers in the demo and toggle dense. Watching the numbers scramble is the whole argument.</p>

<hr />
<h2>Six Properties, Two Questions</h2>
<p>Alignment gets presented as six properties to memorise, and that framing nearly made me skip the section. It's really two questions crossed with three scopes, and once you see it that way there's nothing to memorise.</p>
<p><strong>Which axis?</strong> <code>justify-*</code> is the inline axis, horizontal in English. <code>align-*</code> is the block axis, vertical. This is backwards from the flexbox habit, where the axes swap depending on <code>flex-direction</code>. In grid they stay put, which is one less thing to keep track of.</p>
<p><strong>What moves?</strong> <code>*-items</code> moves the content inside every cell. <code>*-self</code> overrides that for one item. <code>*-content</code> moves the whole set of tracks inside the container, and it only does anything when your tracks are smaller than the container. That's why it looks broken on a grid of <code>1fr</code> tracks that already fill the space. It caught me in the demo below before I worked out what it was for.</p>
<table><thead><tr><th>Property</th><th>Axis</th><th>Moves</th><th>Set on</th></tr></thead><tbody><tr><td>justify-items</td><td>inline</td><td>content within each cell</td><td>container</td></tr><tr><td>align-items</td><td>block</td><td>content within each cell</td><td>container</td></tr><tr><td>justify-self</td><td>inline</td><td>one item in its cell</td><td>item</td></tr><tr><td>align-self</td><td>block</td><td>one item in its cell</td><td>item</td></tr><tr><td>justify-content</td><td>inline</td><td>the whole track grid</td><td>container</td></tr><tr><td>align-content</td><td>block</td><td>the whole track grid</td><td>container</td></tr></tbody></table>
<p><code>place-items</code>, <code>place-self</code> and <code>place-content</code> are shorthands that take <code>align</code> then <code>justify</code>. So <code>place-items: center</code> is the entire centering problem in one line.</p>

<h3>gap</h3>
<p><code>gap</code> is <code>row-gap</code> and <code>column-gap</code> together. It only applies <em>between</em> tracks, never on the outer edges, which is exactly why it beats margins here. No first-child or last-child exception to write. Percentage gaps resolve against the container's own size.</p>
<hr />
<h2>What I Do Not Know Yet</h2>
<p>I wrote this while learning it, not after mastering it, so take the confident bits with a pinch of salt. I haven't lived with a grid layout long enough to know what it costs later, when the design changes, when the content turns out to be messy, when someone else has to edit it. That part's still ahead of me.</p>
<p>I also haven't shipped <code>subgrid</code> yet. It lets a nested grid borrow its parent's track lines instead of making its own. The usual example is a row of cards where every title, body and footer should line up across cards even though the text lengths don't match. Before subgrid that needed fixed heights or JavaScript. It's been available in the major browsers since late 2023, but check your own support floor rather than taking my word for it.</p>
<p>What did change is the question I ask before writing any layout. Not &quot;is this one-dimensional or two-dimensional,&quot; but <strong>who should decide how big this is, the content or the container?</strong> Flexbox was never the wrong tool. It was just the only tool I had, so every problem turned into a content-out problem. Having the other half is the actual difference.</p>
<p>And I've got a gallery to finish. It's still sitting there half-built, except now I can read my own stylesheet and say what each line is doing, which was the whole point of stopping to write this. The build itself is its own write-up, once it's actually done.</p>
<hr />
<h2>Further Reading</h2>
<p>I'm learning this, not authoritative on it, so here's where I checked myself and where you should check me. Browser-support claims especially, they go stale faster than anything else in CSS.</p>
<ul><li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout">MDN — CSS grid layout</a> — the one I keep open while writing this, especially the guides on line-based placement and auto-placement.</li><li><a href="https://www.w3.org/TR/css-grid-2/">CSS Grid Layout Module Level 2 (W3C)</a> — the spec, and the authority on track sizing and why <code>1fr</code> has an <code>auto</code> minimum.</li><li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/minmax">MDN — minmax()</a> — the details behind the <code>minmax(0, 1fr)</code> fix.</li><li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid">MDN — subgrid</a> — current Baseline status, which is the number to trust over any blog post including this one.</li><li><a href="https://caniuse.com/css-grid">Can I use — CSS Grid</a> — for checking support against your own analytics rather than a general claim.</li></ul>]]></content:encoded>
            <author>raviranjan7284@gmail.com (Ravi Ranjan)</author>
            <category>css</category>
            <category>css-grid</category>
            <category>layout</category>
            <category>flexbox</category>
            <category>frontend</category>
            <category>responsive-design</category>
            <enclosure url="https://ravi-ranjan.in/og-image.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[I Built a Restaurant Menu With Flexbox Only — Here's the Challenge]]></title>
            <link>https://ravi-ranjan.in/articles/build-a-restaurant-menu-flexbox-only</link>
            <guid isPermaLink="false">https://ravi-ranjan.in/articles/build-a-restaurant-menu-flexbox-only</guid>
            <pubDate>Wed, 10 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A fully responsive restaurant menu page built with display: flex only — zero CSS Grid. Dietary badges, hover-reveal order buttons, sticky nav, card entrance animations, and a responsive 1/2/3-column layout.]]></description>
            <content:encoded><![CDATA[<h2>The Constraint</h2>
<div class="callout callout-warning"><strong>The Rule</strong><p>Zero <code>display: grid</code> allowed. Every layout problem — card grid, header centering, footer columns, name-price rows — must be solved with <code>display: flex</code> only.</p></div>
<p>In the first challenge I used CSS Grid for the skills section. This time I forced myself to do everything with Flexbox — including a responsive 1/2/3-column card grid. It turns out Flexbox can handle multi-column layouts just fine when you understand <code>flex-basis</code>, <code>flex-wrap</code>, and <code>calc()</code>. I also tackled hover-reveal buttons, dietary badges, sticky navigation, :target highlighting, staggered card entrance animations, and a responsive footer — all with Flexbox as the sole layout mechanism.</p>
<hr />
<h2>What I Built</h2>
<p>A single-page restaurant menu for La Bella Cucina, an upscale Italian restaurant. 15 menu items across 4 categories (Antipasti, Primi Piatti, Secondi Piatti, Dolci), each with a food photo inside <code>&lt;figure&gt;</code>/<code>&lt;figcaption&gt;</code>, Italian name, English description, and price. Cards lift on hover, images zoom, prices change color, and an &quot;Add to Order&quot; button fades in.</p>
<div class="callout callout-tip"><strong>Live Demo</strong><p><a href="/demos/restaurant-menu">See the finished restaurant menu here</a> — sticky nav, dietary badges, hover effects, card animations, and a responsive 1/2/3-column layout. All built with Flexbox only. Try hovering the cards and clicking the nav links.</p></div>
<p>The page breaks down into these sections: a <strong>centered header</strong> with the restaurant name, decorative divider, and tagline. A <strong>sticky category nav</strong> with pill-shaped hover effects. Four <strong>menu sections</strong> each containing a flex-wrapped card grid. Each <strong>menu item card</strong> is both a flex item (of the grid) and a flex container (for its own content). And a <strong>dark footer</strong> that switches from column to row layout at the tablet breakpoint.</p>
<p>Here are the 12 Flexbox properties I used — and nothing else for layout:</p>
<table><thead><tr><th>Property</th><th>Where Used</th><th>What It Does</th></tr></thead><tbody><tr><td>display: flex</td><td>Header, nav, cards, item-header, footer</td><td>Creates a flex container</td></tr><tr><td>flex-direction: column</td><td>Header, card, footer (mobile)</td><td>Stacks children vertically</td></tr><tr><td>flex-direction: row</td><td>Nav, item-header, footer (tablet+)</td><td>Lays children horizontally</td></tr><tr><td>flex-wrap: wrap</td><td>Card grid, footer, item-header</td><td>Items flow to next line</td></tr><tr><td>justify-content: center</td><td>Header, nav</td><td>Centers on main axis</td></tr><tr><td>justify-content: space-between</td><td>Item-header, footer</td><td>Pushes items to edges</td></tr><tr><td>align-items: center</td><td>Header, nav, footer</td><td>Centers on cross axis</td></tr><tr><td>align-items: baseline</td><td>Item-header</td><td>Aligns text baselines</td></tr><tr><td>align-content: flex-start</td><td>Card grid</td><td>Packs wrapped rows at top</td></tr><tr><td>flex-basis + calc()</td><td>Cards</td><td>Sets column widths (100%/50%/33%)</td></tr><tr><td>flex-grow: 1</td><td>Description</td><td>Fills remaining vertical space</td></tr><tr><td>flex-shrink: 0</td><td>Price</td><td>Never compress the price</td></tr></tbody></table>
<hr />
<h2>Step 1 — Project Setup + Design Tokens</h2>
<p>Two files: <code>index.html</code> and <code>style.css</code>. I loaded Google Fonts (Playfair Display for headings, Lato for body text) and defined every design token upfront. The reset includes <code>scroll-behavior: smooth</code> on html for the nav anchor links.</p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot;
          content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;link rel=&quot;preconnect&quot; href=&quot;https://fonts.googleapis.com&quot; /&gt;
    &lt;link rel=&quot;preconnect&quot; href=&quot;https://fonts.gstatic.com&quot;
          crossorigin /&gt;
    &lt;link href=&quot;https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&amp;family=Playfair+Display:wght@400;600;700&amp;display=swap&quot;
          rel=&quot;stylesheet&quot; /&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot; /&gt;
    &lt;title&gt;La Bella Cucina&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;!-- content in next steps --&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<pre><code class="language-css">:root {
  --color-primary: #8B0000;
  --color-cream: #FFF8E7;
  --color-charcoal: #2C2C2C;
  --color-gold: #D4A574;
  --color-olive: #556B2F;
  --color-white: #FFFDF7;
  --color-gold-light: #F5E6D3;
  --color-primary-dark: #5C0000;

  --font-heading: 'Playfair Display', Georgia, serif;
  --font-body: 'Lato', 'Helvetica Neue', Arial, sans-serif;

  --spacing-section: 4rem;
  --spacing-card: 1.25rem;
  --spacing-gap: 1.5rem;
  --spacing-gutter: 2rem;
  --spacing-header: 3rem;

  --shadow-card: 0 2px 8px rgba(44, 44, 44, 0.08);
  --shadow-hover: 0 8px 24px rgba(44, 44, 44, 0.15);

  --radius-card: 8px;
  --radius-btn: 4px;
  --radius-nav: 24px;
}

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  background-color: var(--color-cream);
  font-family: var(--font-body);
  color: var(--color-charcoal);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img {
  display: block;
  max-width: 100%;
  height: auto;
}

a {
  color: inherit;
  text-decoration: none;
}</code></pre>
<div class="callout callout-info"><strong>Why This Matters</strong><p><strong>preconnect hints</strong> — <code>&lt;link rel=&quot;preconnect&quot;&gt;</code> tells the browser to start the DNS + TLS handshake with Google Fonts before it even sees the font request. Saves 100-200ms. <strong>img { display: block }</strong> — Images are inline by default, which adds a mysterious 3-4px gap below them. Since every card has an image, this prevents gaps inside the cards. <strong>scroll-behavior: smooth</strong> — When clicking nav anchor links, the page glides to the target section instead of jumping. One line, zero JavaScript.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>Open index.html — you should see a cream-colored empty page. That's the reset working.</p></div>
<hr />
<h2>Step 2 — Restaurant Header</h2>
<p>Three elements stacked vertically: the restaurant name, a decorative divider, and a tagline. <code>flex-direction: column</code> with <code>align-items: center</code> does the work. I used <code>clamp()</code> for fluid typography — the heading scales from 2rem to 3.5rem based on viewport width.</p>
<pre><code class="language-html">&lt;header class=&quot;restaurant-header&quot;&gt;
  &lt;h1&gt;La Bella Cucina&lt;/h1&gt;
  &lt;span class=&quot;header-divider&quot;&gt;&amp;mdash; Est. 1987 &amp;mdash;&lt;/span&gt;
  &lt;p&gt;Authentic Italian Cuisine Since 1987&lt;/p&gt;
&lt;/header&gt;</code></pre>
<pre><code class="language-css">.restaurant-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-header) var(--spacing-gutter);
  background-color: var(--color-cream);
  text-align: center;
  border-bottom: 1px solid var(--color-gold-light);
}

.restaurant-header h1 {
  font-family: var(--font-heading);
  font-size: clamp(2rem, 5vw, 3.5rem);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0.02em;
  color: var(--color-charcoal);
}

.header-divider {
  display: block;
  font-family: var(--font-body);
  font-size: 0.875rem;
  font-weight: 400;
  letter-spacing: 0.2em;
  color: var(--color-gold);
  margin: 0.75rem 0;
  text-transform: uppercase;
}

.restaurant-header p {
  font-family: var(--font-body);
  font-size: clamp(0.875rem, 1.5vw, 1.125rem);
  font-weight: 300;
  line-height: 1.5;
  letter-spacing: 0.15em;
  color: var(--color-charcoal);
  opacity: 0.7;
  text-transform: uppercase;
}</code></pre>
<div class="callout callout-info"><strong>Why flex-direction: column here?</strong><p>The header has three elements that stack vertically: h1, divider, tagline. <code>column</code> makes the main axis vertical, so <code>align-items: center</code> now centers them horizontally (the cross axis). This is a common source of confusion — <code>justify-content</code> and <code>align-items</code> swap their visual effect when you change direction.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>&quot;La Bella Cucina&quot; centered with a gold divider and a light uppercase tagline below.</p></div>
<hr />
<h2>Step 3 — Sticky Category Navigation</h2>
<p>Four anchor links in a horizontal flex row. <code>position: sticky; top: 0</code> makes the nav stick when you scroll past it. On small screens, <code>overflow-x: auto</code> lets it scroll horizontally instead of wrapping.</p>
<pre><code class="language-html">&lt;nav class=&quot;category-nav&quot;&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;a href=&quot;#antipasti&quot;&gt;Antipasti&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#primi&quot;&gt;Primi Piatti&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#secondi&quot;&gt;Secondi Piatti&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#dolci&quot;&gt;Dolci&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/nav&gt;</code></pre>
<pre><code class="language-css">.category-nav {
  position: sticky;
  top: 0;
  z-index: 100;
  padding: 1rem var(--spacing-gutter);
  background-color: var(--color-gold-light);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-bottom: 2px solid var(--color-gold);
}

.category-nav ul {
  display: flex;
  gap: 0;
  list-style: none;
  margin: 0 auto;
  width: fit-content;
}

.category-nav ul li a {
  display: block;
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-charcoal);
  padding: 0.5rem 1.25rem;
  border-radius: var(--radius-nav);
  transition: background-color 0.2s ease, color 0.2s ease;
  text-wrap: nowrap;
}

.category-nav ul li a:hover {
  background-color: var(--color-primary);
  color: var(--color-cream);
}</code></pre>
<div class="callout callout-info"><strong>sticky vs fixed</strong><p><code>position: sticky</code> only sticks when you scroll to its position — it doesn't cover the header on page load. <code>position: fixed</code> is always anchored to the viewport and removes the element from flow. Sticky is almost always what you want for navigation bars. <strong>border-radius: 24px on the links</strong> — The pill-shaped hover effect is on the <code>&lt;a&gt;</code>, not the <code>&lt;li&gt;</code>. The link has the padding that creates the clickable area. The li is just a structural container.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>Four nav links on a gold bar. Hover — they turn deep red with white text. Scroll down and the nav sticks to the top.</p></div>
<hr />
<h2>Step 4 — Main Container + Section Headings</h2>
<p>Four menu sections, each with an <code>id</code> matching the nav anchors. The section headings use a <code>::after</code> pseudo-element for the gold underline — purely decorative, so it belongs in CSS, not HTML.</p>
<pre><code class="language-html">&lt;main class=&quot;menu-sections&quot;&gt;
  &lt;section id=&quot;antipasti&quot; class=&quot;menu-category&quot;&gt;
    &lt;h2&gt;Antipasti&lt;/h2&gt;
    &lt;ul class=&quot;menu-items&quot;&gt;
      &lt;!-- cards go here in Step 5 --&gt;
    &lt;/ul&gt;
  &lt;/section&gt;

  &lt;section id=&quot;primi&quot; class=&quot;menu-category&quot;&gt;
    &lt;h2&gt;Primi Piatti&lt;/h2&gt;
    &lt;ul class=&quot;menu-items&quot;&gt;&lt;/ul&gt;
  &lt;/section&gt;

  &lt;section id=&quot;secondi&quot; class=&quot;menu-category&quot;&gt;
    &lt;h2&gt;Secondi Piatti&lt;/h2&gt;
    &lt;ul class=&quot;menu-items&quot;&gt;&lt;/ul&gt;
  &lt;/section&gt;

  &lt;section id=&quot;dolci&quot; class=&quot;menu-category&quot;&gt;
    &lt;h2&gt;Dolci&lt;/h2&gt;
    &lt;ul class=&quot;menu-items&quot;&gt;&lt;/ul&gt;
  &lt;/section&gt;
&lt;/main&gt;</code></pre>
<pre><code class="language-css">.menu-sections {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--spacing-gutter);
}

.menu-category {
  padding: var(--spacing-section) 0 0;
  scroll-margin-top: 4rem;
}

.menu-category h2 {
  font-family: var(--font-heading);
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.01em;
  color: var(--color-primary);
  text-align: center;
  margin-bottom: 2rem;
  position: relative;
}

.menu-category h2::after {
  content: '';
  display: block;
  width: 60px;
  height: 2px;
  background: var(--color-gold);
  margin: 0.75rem auto 0;
}</code></pre>
<div class="callout callout-info"><strong>Why This Matters</strong><p><strong>scroll-margin-top: 4rem</strong> — When the nav links scroll to a section, the sticky nav covers the top ~4rem of the viewport. Without scroll-margin-top, the section heading hides behind the nav. This property adds invisible spacing above the scroll target. <strong>max-width + margin: 0 auto</strong> — Content shouldn't stretch across a 2560px ultrawide monitor. max-width caps it. margin: 0 auto centers the block by splitting the remaining space equally between left and right margins.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>Four section headings in deep red, each with a small gold underline, centered within a 1200px container.</p></div>
<hr />
<h2>Step 5 — Menu Item Cards (The Core)</h2>
<p>This is the heart of the project. Each card is both a <strong>flex item</strong> (of the grid) and a <strong>flex container</strong> (for its own content). The grid container uses <code>flex-wrap: wrap</code> so cards flow to the next row. Each card starts at <code>flex-basis: 100%</code> (mobile-first, 1 column).</p>
<p>Here's the HTML pattern for one card — repeat it for each item using the content tables below:</p>
<pre><code class="language-html">&lt;li class=&quot;menu-item&quot;&gt;
  &lt;figure&gt;
    &lt;img src=&quot;https://picsum.photos/seed/bruschetta/400/300&quot;
         alt=&quot;Bruschetta al Pomodoro&quot; /&gt;
    &lt;figcaption&gt;House Favorite&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;div class=&quot;item-header&quot;&gt;
    &lt;h3&gt;Bruschetta al Pomodoro&lt;/h3&gt;
    &lt;span class=&quot;item-price&quot;&gt;$12.95&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;item-description&quot;&gt;
    Grilled bread topped with fresh tomatoes,
    garlic, basil, and extra virgin olive oil
  &lt;/p&gt;
&lt;/li&gt;</code></pre>
<p>Fill all 4 sections: Antipasti (4 items), Primi Piatti (4), Secondi Piatti (4), Dolci (3). Here's the full menu data:</p>
<details><summary>Antipasti — 4 items</summary><table><thead><tr><th>Name</th><th>Badge</th><th>Description</th><th>Price</th><th>Image seed</th></tr></thead><tbody><tr><td>Bruschetta al Pomodoro</td><td>House Favorite</td><td>Grilled bread topped with fresh tomatoes, garlic, basil, and extra virgin olive oil</td><td>$12.95</td><td>bruschetta</td></tr><tr><td>Carpaccio di Manzo</td><td>Chef's Selection</td><td>Thinly sliced raw beef with arugula, capers, shaved Parmigiano, and lemon dressing</td><td>$16.50</td><td>carpaccio</td></tr><tr><td>Calamari Fritti</td><td>Classic</td><td>Lightly battered and fried squid rings served with marinara and lemon aioli</td><td>$14.75</td><td>calamari</td></tr><tr><td>Burrata con Prosciutto</td><td>Seasonal</td><td>Creamy burrata cheese with San Daniele prosciutto, roasted peppers, and basil oil</td><td>$18.00</td><td>burrata</td></tr></tbody></table></details><details><summary>Primi Piatti — 4 items</summary><table><thead><tr><th>Name</th><th>Badge</th><th>Description</th><th>Price</th><th>Image seed</th></tr></thead><tbody><tr><td>Spaghetti alla Carbonara</td><td>Roman Classic</td><td>Traditional Roman pasta with guanciale, egg yolk, Pecorino Romano, and black pepper</td><td>$19.50</td><td>carbonara</td></tr><tr><td>Risotto ai Funghi Porcini</td><td>Seasonal Special</td><td>Arborio rice slow-cooked with wild porcini mushrooms, white wine, and aged Parmigiano</td><td>$22.00</td><td>risotto</td></tr><tr><td>Pappardelle al Ragu</td><td>House Made</td><td>Wide ribbon pasta with slow-braised Tuscan beef and pork ragu, finished with ricotta</td><td>$21.50</td><td>pappardelle</td></tr><tr><td>Gnocchi alla Sorrentina</td><td>Comfort Dish</td><td>Potato gnocchi baked with San Marzano tomato sauce, fresh mozzarella, and basil</td><td>$18.75</td><td>gnocchi</td></tr></tbody></table></details><details><summary>Secondi Piatti — 4 items</summary><table><thead><tr><th>Name</th><th>Badge</th><th>Description</th><th>Price</th><th>Image seed</th></tr></thead><tbody><tr><td>Osso Buco alla Milanese</td><td>Signature Dish</td><td>Braised veal shank with saffron risotto, gremolata, and root vegetables</td><td>$34.00</td><td>ossobuco</td></tr><tr><td>Branzino al Forno</td><td>Market Fresh</td><td>Whole roasted Mediterranean sea bass with capers, olives, cherry tomatoes, and herbs</td><td>$32.50</td><td>branzino</td></tr><tr><td>Saltimbocca alla Romana</td><td>Roman Classic</td><td>Veal cutlets wrapped with prosciutto and sage, pan-seared in white wine butter sauce</td><td>$28.75</td><td>saltimbocca</td></tr><tr><td>Pollo alla Parmigiana</td><td>Family Recipe</td><td>Breaded chicken breast baked with San Marzano sauce, mozzarella, and fresh basil</td><td>$24.50</td><td>parmigiana</td></tr></tbody></table></details><details><summary>Dolci — 3 items</summary><table><thead><tr><th>Name</th><th>Badge</th><th>Description</th><th>Price</th><th>Image seed</th></tr></thead><tbody><tr><td>Tiramisu Classico</td><td>Signature Dessert</td><td>Espresso-soaked ladyfingers layered with mascarpone cream and dusted with cocoa</td><td>$12.00</td><td>tiramisu</td></tr><tr><td>Panna Cotta ai Frutti di Bosco</td><td>Seasonal</td><td>Vanilla bean panna cotta topped with mixed berry compote and fresh mint</td><td>$10.50</td><td>pannacotta</td></tr><tr><td>Cannoli Siciliani</td><td>Traditional</td><td>Crispy pastry shells filled with sweet ricotta, chocolate chips, and candied orange peel</td><td>$11.00</td><td>cannoli</td></tr></tbody></table></details>
<p>All image URLs follow the pattern: <code>https://picsum.photos/seed/{seed}/400/300</code></p>
<p>Now the CSS — this is where Flexbox does its heaviest lifting:</p>
<pre><code class="language-css">/* The flex grid container */
.menu-items {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-gap);
  align-content: flex-start;
  list-style: none;
}

/* Each card — flex ITEM of grid + flex CONTAINER for content */
.menu-item {
  display: flex;
  flex-direction: column;
  flex-basis: 100%;          /* Mobile: 1 column */
  flex-grow: 0;              /* Don't stretch wider */
  flex-shrink: 1;
  background: var(--color-white);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  transition: box-shadow 0.3s ease, transform 0.3s ease;
}

/* Figure + image */
.menu-item figure {
  position: relative;
  overflow: hidden;
}

.menu-item figure img {
  width: 100%;
  height: 220px;
  object-fit: cover;
  transition: transform 0.4s ease;
}

.menu-item figure figcaption {
  position: absolute;
  top: 12px;
  left: 12px;
  background-color: var(--color-olive);
  color: var(--color-cream);
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius-btn);
}

/* Name + price row */
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0.85rem var(--spacing-card) 0;
}

.item-header h3 {
  font-family: var(--font-heading);
  font-size: clamp(1rem, 1.5vw, 1.25rem);
  font-weight: 600;
  line-height: 1.3;
  color: var(--color-charcoal);
}

.item-price {
  font-family: var(--font-body);
  font-size: clamp(1rem, 1.2vw, 1.125rem);
  font-weight: 700;
  color: var(--color-primary);
  flex-shrink: 0;
  white-space: nowrap;
  transition: color 0.2s ease;
}

/* Description — flex-grow: 1 is KEY */
.item-description {
  flex-grow: 1;
  padding: 0.5rem var(--spacing-card) var(--spacing-card);
  font-family: var(--font-body);
  font-size: clamp(0.8rem, 1vw, 0.9375rem);
  font-weight: 400;
  line-height: 1.6;
  color: var(--color-charcoal);
  opacity: 0.8;
}</code></pre>
<div class="callout callout-info"><strong>Key Flexbox Concepts</strong><p><strong>flex-grow: 0 on cards</strong> — If a row has only 1 card (like the 4th card in a 3-column section), flex-grow: 0 prevents it from stretching to fill the whole row. <strong>flex-shrink: 0 on price</strong> — A long dish name like &quot;Panna Cotta ai Frutti di Bosco&quot; could push the price and cause it to shrink. flex-shrink: 0 says &quot;never shrink the price, let the name wrap instead.&quot; <strong>flex-grow: 1 on description</strong> — This is the key trick for equal-height cards. Cards in a row all stretch to the tallest card's height (because the parent defaults to align-items: stretch). flex-grow: 1 on the description makes it absorb the extra height, so all cards look uniform. <strong>align-items: baseline on item-header</strong> — The h3 and price have different font sizes. baseline aligns their text baselines so they read as one line. center would misalign them vertically. <strong>object-fit: cover on images</strong> — The source images are 400×300 but displayed at full-width × 220px. cover fills the space and crops the overflow — no stretching or distortion.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>15 cards stacked in a single column. Each has an image with a green badge overlay, an Italian name with price on the right, and an English description below.</p></div>
<hr />
<h2>Step 6 — Card Hover Effects</h2>
<p>Three things happen on hover: the card lifts up with a deeper shadow, the image zooms 8%, and the price changes from red to gold. All using <code>transform</code> (GPU-accelerated) instead of changing width/height (which triggers layout reflow).</p>
<pre><code class="language-css">.menu-item:hover {
  box-shadow: var(--shadow-hover);
  transform: translateY(-4px);
}

.menu-item:hover figure img {
  transform: scale(1.08);
}

.menu-item:hover .item-price {
  color: var(--color-gold);
}</code></pre>
<div class="callout callout-info"><strong>Why overflow: hidden matters</strong><p>The image zooms to 108% on hover. Without <code>overflow: hidden</code> on the card and figure (set in Step 5), the zoomed image would overflow the card's rounded corners. The card clips it.</p></div>
<hr />
<h2>Step 7 — Responsive Breakpoints (2-column, 3-column)</h2>
<p>The base CSS is mobile-first (1 column). Media queries add complexity at larger sizes. The math for <code>flex-basis</code> accounts for the gap between cards.</p>
<pre><code class="language-css">/* Tablet: 2 columns */
@media (min-width: 640px) {
  .menu-item {
    flex-basis: calc(50% - var(--spacing-gap) / 2);
  }
}

/* Desktop: 3 columns */
@media (min-width: 1024px) {
  .menu-item {
    flex-basis: calc(33.333% - var(--spacing-gap) * 2 / 3);
  }

  .menu-item figure img {
    height: 200px;
  }
}</code></pre>
<div class="callout callout-info"><strong>The calc() Math</strong><p><strong>2 columns:</strong> Two cards per row with one gap between them. Each card = 50% minus half the gap. If gap = 1.5rem, each card is <code>50% - 0.75rem</code>. <strong>3 columns:</strong> Three cards per row with two gaps. Total gap space = 1.5rem × 2 = 3rem, divided by 3 cards = 1rem per card. Each is <code>33.333% - 1rem</code>. <strong>Why mobile-first?</strong> The base CSS is for the smallest screen. You only add complexity at larger sizes. If media queries fail to load, mobile users still get a working layout.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>Resize your browser: below 640px = 1 column, 640-1023px = 2 columns, 1024px+ = 3 columns.</p></div>
<hr />
<h2>Step 8 — Footer</h2>
<p>The footer is another flex container that switches direction at the tablet breakpoint — column on mobile, row on tablet+. The copyright uses <code>flex-basis: 100%</code> to force itself onto its own line in the wrapping row.</p>
<pre><code class="language-html">&lt;footer class=&quot;site-footer&quot;&gt;
  &lt;div class=&quot;footer-address&quot;&gt;
    &lt;p&gt;127 Via Roma, Greenwich Village, New York, NY 10012&lt;/p&gt;
    &lt;p&gt;(212) 555-0187&lt;/p&gt;
  &lt;/div&gt;
  &lt;div class=&quot;footer-hours&quot;&gt;
    &lt;p&gt;Mon&amp;ndash;Thu 5:00 PM &amp;ndash; 10:00 PM&lt;/p&gt;
    &lt;p&gt;Fri&amp;ndash;Sat 5:00 PM &amp;ndash; 11:00 PM&lt;/p&gt;
    &lt;p&gt;Sun 4:00 PM &amp;ndash; 9:00 PM&lt;/p&gt;
  &lt;/div&gt;
  &lt;div class=&quot;footer-cta&quot;&gt;
    &lt;a href=&quot;#&quot; class=&quot;reserve-btn&quot;&gt;Reserve a Table&lt;/a&gt;
  &lt;/div&gt;
  &lt;p class=&quot;footer-copyright&quot;&gt;
    &amp;copy; 2024 La Bella Cucina. All rights reserved.
  &lt;/p&gt;
&lt;/footer&gt;</code></pre>
<pre><code class="language-css">.site-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 1.5rem;
  padding: var(--spacing-section) var(--spacing-gutter);
  margin-top: var(--spacing-section);
  background: var(--color-charcoal);
  color: var(--color-cream);
  font-family: var(--font-body);
  font-size: 0.9rem;
  line-height: 1.7;
}

.footer-address p,
.footer-hours p {
  opacity: 0.85;
}

.footer-hours {
  border-top: 1px solid rgba(255, 255, 255, 0.15);
  border-bottom: 1px solid rgba(255, 255, 255, 0.15);
  padding: 1rem 0;
}

.reserve-btn {
  display: inline-block;
  background-color: var(--color-primary);
  color: var(--color-cream);
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.85rem 2rem;
  border-radius: var(--radius-btn);
  transition: background-color 0.2s ease, transform 0.1s ease;
}

.reserve-btn:hover {
  background-color: var(--color-primary-dark);
  transform: translateY(-1px);
}

.footer-copyright {
  font-size: 0.8rem;
  opacity: 0.5;
}

/* Tablet+: horizontal footer */
@media (min-width: 640px) {
  .site-footer {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    text-align: left;
    flex-wrap: wrap;
  }

  .footer-hours {
    border-top: none;
    border-bottom: none;
    border-left: 1px solid rgba(255, 255, 255, 0.15);
    border-right: 1px solid rgba(255, 255, 255, 0.15);
    padding: 0 1.5rem;
  }

  .footer-copyright {
    flex-basis: 100%;
    text-align: center;
    margin-top: 0.5rem;
  }
}</code></pre>
<div class="callout callout-info"><strong>flex-basis: 100% trick</strong><p>In a wrapping flex row, an item with <code>flex-basis: 100%</code> forces itself onto its own line. This puts the copyright centered below the three main footer columns — no extra wrapper needed.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>Dark footer with address, hours, and a red reserve button. Stacks on mobile, goes horizontal with vertical dividers on tablet+.</p></div>
<hr />
<h2>Step 9 — Dietary Badges (V, GF)</h2>
<p>Some dishes need dietary badges — V for Vegetarian, GF for Gluten Free. These go in the item-header between the name and price. The key is adding <code>flex-wrap: wrap</code> and adjusting the gap on item-header so badges can sit next to the dish name and the price pushes to the end.</p>
<p>Update the item-header HTML for dishes that have badges:</p>
<pre><code class="language-html">&lt;div class=&quot;item-header&quot;&gt;
  &lt;h3&gt;Bruschetta al Pomodoro&lt;/h3&gt;
  &lt;span class=&quot;badge badge-v&quot;&gt;V&lt;/span&gt;
  &lt;span class=&quot;item-price&quot;&gt;$12.95&lt;/span&gt;
&lt;/div&gt;

&lt;!-- For items with multiple badges: --&gt;
&lt;div class=&quot;item-header&quot;&gt;
  &lt;h3&gt;Burrata con Prosciutto&lt;/h3&gt;
  &lt;span class=&quot;badge badge-v&quot;&gt;V&lt;/span&gt;
  &lt;span class=&quot;badge badge-gf&quot;&gt;GF&lt;/span&gt;
  &lt;span class=&quot;item-price&quot;&gt;$18.00&lt;/span&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">/* Update item-header for badges */
.item-header {
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
}

.badge {
  font-family: var(--font-body);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 0.15rem 0.4rem;
  border-radius: var(--radius-btn);
}

.badge-v {
  background-color: var(--color-olive);
  color: var(--color-cream);
}

.badge-gf {
  background-color: var(--color-gold);
  color: var(--color-charcoal);
}</code></pre>
<div class="callout callout-tip"><strong>Which items get badges?</strong><p><strong>V (Vegetarian):</strong> Bruschetta, Burrata, Risotto, Gnocchi, Tiramisu, Panna Cotta, Cannoli. <strong>GF (Gluten Free):</strong> Carpaccio, Burrata, Risotto, Osso Buco, Branzino, Saltimbocca, Panna Cotta.</p></div>
<hr />
<h2>Step 10 — Hover-Reveal Order Button</h2>
<p>An &quot;Add to Order&quot; button at the bottom of each card that's invisible by default and fades in + slides up on hover. This uses <code>opacity: 0</code> plus <code>transform: translateY(4px)</code> on the button, then the card's :hover state reveals it.</p>
<pre><code class="language-html">&lt;!-- Add at the end of each &lt;li class=&quot;menu-item&quot;&gt;, 
     after the .item-description paragraph --&gt;
&lt;button class=&quot;order-btn&quot;&gt;Add to Order&lt;/button&gt;</code></pre>
<pre><code class="language-css">.order-btn {
  margin: 0 var(--spacing-card) var(--spacing-card);
  padding: 0.5rem 1rem;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background-color: var(--color-primary);
  color: var(--color-cream);
  border: none;
  border-radius: var(--radius-btn);
  cursor: pointer;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.25s ease,
             transform 0.25s ease,
             background-color 0.2s ease;
}

.menu-item:hover .order-btn {
  opacity: 1;
  transform: translateY(0);
}

.order-btn:hover {
  background-color: var(--color-primary-dark);
}</code></pre>
<div class="callout callout-info"><strong>Why opacity + transform?</strong><p>The button is always in the DOM — it takes up its space even when invisible. Using <code>display: none</code> would cause a layout shift on hover as the button appears and pushes content around. <code>opacity: 0</code> keeps the button's space reserved. The slight <code>translateY(4px)</code> adds a subtle slide-up motion when it fades in.</p></div>
<hr />
<h2>Step 11 — :target Highlighting + Smooth Scroll</h2>
<p>When you click a nav link like &quot;Antipasti,&quot; the URL becomes <code>#antipasti</code> and the <code>:target</code> pseudo-class matches that section. I used this to change the heading color to gold and expand the underline — a visual confirmation that navigation happened.</p>
<pre><code class="language-css">/* :target fires when the section's id matches the URL hash */
.menu-category:target h2 {
  color: var(--color-gold);
  transition: color 0.3s ease;
}

.menu-category:target h2::after {
  width: 120px;
  background: var(--color-primary);
  transition: width 0.3s ease, background 0.3s ease;
}</code></pre>
<div class="callout callout-info"><strong>How :target works</strong><p>The <code>:target</code> pseudo-class matches the element whose id equals the current URL fragment (the part after #). When you click <code>&lt;a href=&quot;#antipasti&quot;&gt;</code>, the URL becomes <code>page.html#antipasti</code> and <code>#antipasti:target</code> matches. Combined with <code>scroll-behavior: smooth</code> from Step 1, clicking a nav link smoothly scrolls to the section and highlights the heading.</p></div>
<hr />
<h2>Step 12 — Card Entrance Animations</h2>
<p>Cards fade in and slide up on page load with staggered delays. The first card appears immediately, the second after 0.1s, the third after 0.2s, and so on. This creates a cascading entrance effect.</p>
<pre><code class="language-css">@keyframes cardFadeIn {
  0% {
    opacity: 0;
    transform: translateY(20px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

.menu-item {
  animation: cardFadeIn 0.6s ease both;
}

.menu-item:nth-child(1) { animation-delay: 0s; }
.menu-item:nth-child(2) { animation-delay: 0.1s; }
.menu-item:nth-child(3) { animation-delay: 0.2s; }
.menu-item:nth-child(4) { animation-delay: 0.3s; }</code></pre>
<div class="callout callout-info"><strong>animation-fill-mode: both</strong><p>The keyword <code>both</code> in the animation shorthand sets <code>animation-fill-mode: both</code>. This means the element stays at the 0% state before the animation starts (invisible + shifted down) and stays at the 100% state after it ends (visible + normal position). Without it, cards would flash visible before the delayed animation starts.</p></div>
<hr />
<h2>Acceptance Criteria</h2>
<p>Here's the checklist I used to verify the build:</p>
<ul><li>Zero display: grid in the stylesheet</li><li>Uses display: flex as the sole layout mechanism</li><li>Uses flex-direction (both row and column)</li><li>Uses justify-content with at least 2 values (center, space-between)</li><li>Uses align-items with at least 2 values (center, baseline)</li><li>Uses align-content: flex-start on the card grid</li><li>Uses flex-wrap: wrap for responsive card layout</li><li>Uses flex-grow: 1 on the description for equal-height cards</li><li>Uses flex-shrink: 0 on the price</li><li>Uses flex-basis with calc() for responsive columns</li><li>Uses gap for spacing between cards</li><li>Responsive: 1 col (&lt; 640px), 2 col (640–1023px), 3 col (&gt;= 1024px)</li><li>All images use &lt;figure&gt; and &lt;figcaption&gt;</li><li>4 menu sections with 15 total items</li><li>Hover effects: shadow lift, image zoom, price color change</li><li>Sticky category nav with pill-shaped hover</li><li>Dietary badges (V and GF) with flex-wrap on item-header</li><li>Hover-reveal &quot;Add to Order&quot; button with opacity + transform transition</li><li>:target highlighting on section headings</li><li>Card entrance animations with staggered delays</li><li>Footer switches from column to row at 640px</li><li>All CSS values reference design token variables from :root</li><li>Google Fonts loaded: Playfair Display + Lato</li></ul>
<hr />
<h2>Flexbox Properties Used</h2>
<h3>Quick Reference: flex-grow, flex-shrink, flex-basis</h3>
<table><thead><tr><th>Property</th><th>Default</th><th>What It Does</th></tr></thead><tbody><tr><td>flex-basis</td><td>auto</td><td>Starting size before growing/shrinking (like &quot;preferred width&quot;)</td></tr><tr><td>flex-grow</td><td>0</td><td>How leftover space is distributed (0 = don't grow, 1 = take your share)</td></tr><tr><td>flex-shrink</td><td>1</td><td>How items give up space when too tight (0 = refuse to shrink)</td></tr></tbody></table>
<h3>justify-content vs align-items</h3>
<p><code>justify-content</code> controls spacing along the main axis (the direction items flow). <code>align-items</code> controls positioning along the cross axis (perpendicular). When <code>flex-direction: row</code>, main = horizontal and cross = vertical. When <code>flex-direction: column</code>, they swap. That's why <code>align-items: center</code> centers horizontally in the header (column direction) but vertically in the footer (row direction).</p>
<h3>The flex-basis + calc() Pattern for Columns</h3>
<table><thead><tr><th>Columns</th><th>Formula</th><th>Result (with 1.5rem gap)</th></tr></thead><tbody><tr><td>1</td><td>flex-basis: 100%</td><td>Full width</td></tr><tr><td>2</td><td>calc(50% - gap / 2)</td><td>50% - 0.75rem</td></tr><tr><td>3</td><td>calc(33.333% - gap * 2 / 3)</td><td>33.333% - 1rem</td></tr><tr><td>4</td><td>calc(25% - gap * 3 / 4)</td><td>25% - 1.125rem</td></tr></tbody></table>]]></content:encoded>
            <author>raviranjan7284@gmail.com (Ravi Ranjan)</author>
            <category>html</category>
            <category>css</category>
            <category>flexbox</category>
            <category>responsive</category>
            <category>semantic-html</category>
            <category>css-custom-properties</category>
            <category>beginner</category>
        </item>
        <item>
            <title><![CDATA[I Built a Profile Card With Zero Divs — Here's the Challenge]]></title>
            <link>https://ravi-ranjan.in/articles/build-a-profile-card-zero-divs</link>
            <guid isPermaLink="false">https://ravi-ranjan.in/articles/build-a-profile-card-zero-divs</guid>
            <pubDate>Wed, 03 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I built a personal profile card using only semantic HTML5 elements — zero <div> tags. Dark mode, skill meters, animated entrance, print stylesheet — all with no JavaScript.]]></description>
            <content:encoded><![CDATA[<h2>The Constraint</h2>
<div class="callout callout-warning"><strong>The Rule</strong><p>Zero <code>&lt;div&gt;</code> tags allowed. Every piece of content must use a semantic HTML5 element. If you reach for a <code>&lt;div&gt;</code>, stop and find the right element.</p></div>
<p>I used to wrap everything in <code>&lt;div&gt;</code> tags. It works, but it tells the browser — and screen readers — nothing about the content. So I gave myself this constraint to learn the right element for each role. By the end, I used 7 semantic elements, 5 CSS selector types, CSS Grid for skills, <code>:has()</code> for dark mode, <code>&lt;meter&gt;</code> for skill proficiency bars, and a print stylesheet — all with zero JavaScript. Here's the full walkthrough — and the challenge for you to try it yourself.</p>
<hr />
<h2>What I Built</h2>
<p>A centered profile card on a dark page — white rectangle, max-width 480px, rounded corners, shadow, animated entrance. It features a dark mode toggle (no JS), skill proficiency meters, link icons via <code>::before</code> pseudo-elements, and a print stylesheet. Here's the final result:</p>
<div class="callout callout-tip"><strong>Live Demo</strong><p><a href="/demos/profile-card">See the finished profile card here</a> — circular photo, dark mode toggle, skill grid with proficiency meters, contact links with icons, animated fade-in, and a footer. All built with zero &lt;div&gt; tags. Think you can build it? Follow along.</p></div>
<p>I broke the card into these sections from top to bottom: a <strong>dark mode toggle</strong> using <code>&lt;details&gt;</code>. A <strong>header</strong> with the photo, name, and title. An <strong>About Me</strong> section with a short bio. A <strong>Skills</strong> section with a 4-column grid of icons, names, and <code>&lt;meter&gt;</code> proficiency bars. A <strong>Fun Fact</strong> callout box. A <strong>Contact</strong> section with icon-prefixed links. And a <strong>footer</strong> with copyright text.</p>
<p>I used these 8 semantic elements — no <code>&lt;div&gt;</code> anywhere:</p>
<table><thead><tr><th>Element</th><th>Meaning</th><th>Where</th></tr></thead><tbody><tr><td>&lt;main&gt;</td><td>Primary page content (one per page)</td><td>Wraps the entire card</td></tr><tr><td>&lt;article&gt;</td><td>Self-contained content</td><td>The profile card itself</td></tr><tr><td>&lt;header&gt;</td><td>Introductory content for its parent</td><td>Photo + name + title</td></tr><tr><td>&lt;details&gt;</td><td>Disclosure widget (open/close)</td><td>Dark mode toggle</td></tr><tr><td>&lt;section&gt;</td><td>Thematic grouping with a heading</td><td>About, Skills sections</td></tr><tr><td>&lt;aside&gt;</td><td>Tangentially related content</td><td>Fun fact callout</td></tr><tr><td>&lt;nav&gt;</td><td>Navigation links</td><td>Contact links</td></tr><tr><td>&lt;footer&gt;</td><td>Footer for its parent</td><td>Copyright notice</td></tr><tr><td>&lt;figure&gt;</td><td>Self-contained media</td><td>Profile photo</td></tr><tr><td>&lt;meter&gt;</td><td>Scalar measurement within a range</td><td>Skill proficiency bars</td></tr></tbody></table>
<hr />
<h2>Step 1 — Project Setup + CSS Reset</h2>
<p>I started with two files: <code>index.html</code> and <code>styles.css</code>. The foundation — boilerplate every project starts with.</p>
<p><strong>Challenge:</strong> Try writing the HTML boilerplate yourself — with <code>lang=&quot;en&quot;</code>, the viewport meta tag, and a linked stylesheet. Then check my version below.</p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot;
          content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;styles.css&quot; /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;!-- content goes here in next steps --&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3>Design Tokens</h3>
<p>I defined every color, size, and spacing value as CSS custom properties upfront. This is the full design token block I used:</p>
<pre><code class="language-css">*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --color-bg: #1a1a2e;
  --color-card-bg: #ffffff;
  --color-text-primary: #16213e;
  --color-text-body: #4a4a68;
  --color-text-muted: #8888a0;
  --color-accent: #e94560;
  --color-accent-hover: #c81e45;
  --color-skill-bg: #f0f0f5;
  --color-aside-bg: #f8f8fc;
  --color-border: #e0e0e8;

  --card-padding: 2.5rem;
  --section-gap: 1.5rem;
  --card-radius: 16px;
  --photo-size: 160px;
}</code></pre>
<div class="callout callout-info"><strong>Why This Matters</strong><p><strong>lang=&quot;en&quot;</strong> — Screen readers use this to choose pronunciation rules. Without it, a French screen reader might read English with French pronunciation. <strong>box-sizing: border-box</strong> — Without it, width: 200px + padding: 20px + border: 2px = 244px total. With border-box, 200px means 200px. <strong>viewport meta</strong> — Without it, mobile browsers render at ~980px width and zoom out.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>At this point, opening index.html shows a blank white page. That's correct — the reset is working.</p></div>
<hr />
<h2>Step 2 — Page Background + Card Centering</h2>
<p>Next I needed the dark navy page with a white card centered in the middle. Flexbox does the heavy lifting here. I wrapped the content in <code>&lt;main&gt;</code> and <code>&lt;article&gt;</code> — no <code>&lt;div&gt;</code> needed.</p>
<pre><code class="language-html">&lt;body&gt;
  &lt;main&gt;
    &lt;article class=&quot;profile-card&quot;&gt;
      &lt;!-- card content in next steps --&gt;
    &lt;/article&gt;
  &lt;/main&gt;
&lt;/body&gt;</code></pre>
<pre><code class="language-css">body {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: var(--color-bg);
  font-family: &quot;Segoe UI&quot;, Tahoma, Geneva, Verdana, sans-serif;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--color-text-body);
  padding: 2rem;
}

article {
  position: relative;
  background-color: var(--color-card-bg);
  max-width: 480px;
  width: 100%;
  border-radius: var(--card-radius);
  padding: var(--card-padding);
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
  transition: box-shadow 0.3s ease;
  animation-name: fadeSlideUp;
  animation-duration: 1s;
}</code></pre>
<div class="callout callout-info"><strong>Why This Matters</strong><p><strong>&lt;main&gt;</strong> — Every page must have exactly one. Tells screen readers &quot;this is the primary content&quot; so users can skip directly to it. <strong>&lt;article&gt;</strong> — Self-contained content that could be extracted and placed on another page and still make sense. That's the test for using &lt;article&gt;. <strong>position: relative</strong> — Needed so the dark mode toggle can be positioned absolutely inside the card. <strong>animation-name: fadeSlideUp</strong> — The card fades in and slides up on load. We'll define the keyframes in Step 10.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>Dark navy page with a white rounded rectangle centered in the middle. Empty for now — but the structure is solid.</p></div>
<hr />
<h2>Step 3 — Dark Mode Toggle + Card Header</h2>
<p>I added a dark mode toggle using <code>&lt;details&gt;</code> — a native HTML disclosure widget. When it's open, CSS <code>:has()</code> detects the state and swaps all the design tokens. No JavaScript needed. Then the header with the circular photo, bold name, and muted job title.</p>
<pre><code class="language-html">&lt;details class=&quot;theme-toggle&quot;&gt;
  &lt;summary&gt;Toggle Dark Mode&lt;/summary&gt;
&lt;/details&gt;
&lt;header&gt;
  &lt;figure class=&quot;profile-photo&quot;&gt;
    &lt;img src=&quot;https://imgcdn.stablediffusionweb.com/2024/12/7/d88a43ab-ab0d-4462-bfed-3ee4f1e76e78.jpg&quot;
         alt=&quot;Sarah Chen&quot; /&gt;
  &lt;/figure&gt;
  &lt;h1&gt;Sarah Chen&lt;/h1&gt;
  &lt;p&gt;Software Engineer and Web Developer&lt;/p&gt;
&lt;/header&gt;</code></pre>
<pre><code class="language-css">/* Dark mode toggle */
.theme-toggle {
  position: absolute;
  top: 1rem;
  right: 1rem;
}

.theme-toggle summary {
  list-style: none;
  cursor: pointer;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: var(--color-skill-bg);
  border: 1px solid var(--color-border);
  transition: background 0.2s ease;
  text-indent: -9999px;
  overflow: hidden;
  position: relative;
}

.theme-toggle summary::before {
  content: &quot;\1F319&quot;;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  text-indent: 0;
  font-size: 1.125rem;
}

.theme-toggle[open] summary::before {
  content: &quot;\2600\FE0F&quot;;
}

.theme-toggle summary:hover {
  background: var(--color-border);
}

.theme-toggle summary::-webkit-details-marker {
  display: none;
}

/* Profile photo */
.profile-photo img {
  width: 160px;
  height: 160px;
  border-radius: 50%;
  object-fit: cover;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* Name and title */
.profile-card header h1 {
  font-size: 1.75rem;
  font-weight: 700;
  color: var(--color-text-primary);
  margin-top: 1rem;
}

.profile-card header p {
  font-size: 1rem;
  color: var(--color-text-muted);
}</code></pre>
<div class="callout callout-info"><strong>Why This Matters</strong><p><strong>&lt;details&gt; for dark mode</strong> — A native HTML element that toggles open/closed state without JavaScript. The <code>summary</code> is visually hidden behind a moon emoji using <code>text-indent: -9999px</code> and <code>::before</code> pseudo-element. <strong>object-fit: cover</strong> — The source image is rectangular but displayed at 160×160. cover fills and crops. Without it, the image would be stretched.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>White card with a moon icon in the top-right corner, circular photo centered at the top, &quot;Sarah Chen&quot; in bold below, &quot;Software Engineer and Web Developer&quot; in gray.</p></div>
<hr />
<h2>Step 4 — About Section</h2>
<p>A thematic grouping with a heading — exactly what <code>&lt;section&gt;</code> is for. I styled the heading as a small uppercase label to act as a section marker.</p>
<pre><code class="language-html">&lt;section&gt;
  &lt;h2&gt;About Me&lt;/h2&gt;
  &lt;p&gt;
    I'm a full-stack developer based in Toronto
    with 5 years of experience building web
    applications. I specialize in React and
    Node.js ecosystems, with a focus on
    performance and accessibility. Previously, I
    worked at Shopify building merchant-facing
    tools. Currently, I'm exploring AI-powered
    developer tooling and contributing to open
    source.
  &lt;/p&gt;
&lt;/section&gt;</code></pre>
<pre><code class="language-css">section h2,
nav h2 {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1rem;
  color: var(--color-text-muted);
  margin-bottom: 0.75rem;
  margin-top: 1rem;
}</code></pre>
<div class="callout callout-tip"><strong>section vs article</strong><p>A <code>&lt;section&gt;</code> is a thematic grouping that needs its parent for context. The &quot;About Me&quot; section only makes sense as part of this card. An <code>&lt;article&gt;</code> stands alone — you could paste it on any other page and it still makes sense.</p></div>
<hr />
<h2>Step 5 — Section Dividers (The + Combinator)</h2>
<p>No extra HTML needed here. I used the adjacent sibling combinator in CSS to add borders between sections without giving the first section an unwanted top border.</p>
<pre><code class="language-css">/* Adjacent sibling combinator (+):
   targets an element that immediately follows
   another at the same nesting level */

section + section,
section + aside,
aside + nav {
  border-top: 1px solid var(--color-border);
  padding-top: var(--section-gap);
  margin-top: var(--section-gap);
}

section + aside {
  padding-top: 1rem;
  border-top: none;
}</code></pre>
<div class="callout callout-info"><strong>Why the + combinator?</strong><p><code>section + section</code> means &quot;a section that directly follows another section.&quot; It skips the first one — no unwanted top border on About. Note that <code>section + aside</code> removes the top border but keeps the spacing — the aside sits tight against the skills section. <strong>Borders in px:</strong> Borders should be exactly 1px. Using rem could round to 0px or 2px at different font sizes.</p></div>
<hr />
<h2>Step 6 — Skills Grid (CSS Grid + &lt;meter&gt;)</h2>
<p>This was a fun one — instead of simple pill badges, I used a 4-column CSS Grid with skill icons (emoji), names, and <code>&lt;meter&gt;</code> elements for proficiency bars. Each skill card has a hover effect that lifts it slightly.</p>
<pre><code class="language-html">&lt;section&gt;
  &lt;h2&gt;Skills&lt;/h2&gt;
  &lt;ul class=&quot;skills-list&quot;&gt;
    &lt;li&gt;
      &lt;span class=&quot;skill-icon&quot; aria-hidden=&quot;true&quot;&gt;&amp;#9883;&lt;/span&gt;
      &lt;span class=&quot;skill-name&quot;&gt;React&lt;/span&gt;
      &lt;meter min=&quot;0&quot; max=&quot;100&quot; value=&quot;90&quot;&gt;90%&lt;/meter&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;span class=&quot;skill-icon&quot; aria-hidden=&quot;true&quot;&gt;&amp;#9881;&lt;/span&gt;
      &lt;span class=&quot;skill-name&quot;&gt;Node.js&lt;/span&gt;
      &lt;meter min=&quot;0&quot; max=&quot;100&quot; value=&quot;85&quot;&gt;85%&lt;/meter&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;span class=&quot;skill-icon&quot; aria-hidden=&quot;true&quot;&gt;&amp;#10094;&amp;#10095;&lt;/span&gt;
      &lt;span class=&quot;skill-name&quot;&gt;JavaScript&lt;/span&gt;
      &lt;meter min=&quot;0&quot; max=&quot;100&quot; value=&quot;95&quot;&gt;95%&lt;/meter&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;span class=&quot;skill-icon&quot; aria-hidden=&quot;true&quot;&gt;&amp;#9998;&lt;/span&gt;
      &lt;span class=&quot;skill-name&quot;&gt;HTML/CSS&lt;/span&gt;
      &lt;meter min=&quot;0&quot; max=&quot;100&quot; value=&quot;90&quot;&gt;90%&lt;/meter&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;span class=&quot;skill-icon&quot; aria-hidden=&quot;true&quot;&gt;&amp;#128013;&lt;/span&gt;
      &lt;span class=&quot;skill-name&quot;&gt;Python&lt;/span&gt;
      &lt;meter min=&quot;0&quot; max=&quot;100&quot; value=&quot;60&quot;&gt;60%&lt;/meter&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;span class=&quot;skill-icon&quot; aria-hidden=&quot;true&quot;&gt;&amp;#128451;&lt;/span&gt;
      &lt;span class=&quot;skill-name&quot;&gt;SQL&lt;/span&gt;
      &lt;meter min=&quot;0&quot; max=&quot;100&quot; value=&quot;70&quot;&gt;70%&lt;/meter&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;span class=&quot;skill-icon&quot; aria-hidden=&quot;true&quot;&gt;&amp;#127811;&lt;/span&gt;
      &lt;span class=&quot;skill-name&quot;&gt;MongoDB&lt;/span&gt;
      &lt;meter min=&quot;0&quot; max=&quot;100&quot; value=&quot;65&quot;&gt;65%&lt;/meter&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;span class=&quot;skill-icon&quot; aria-hidden=&quot;true&quot;&gt;&amp;#9729;&lt;/span&gt;
      &lt;span class=&quot;skill-name&quot;&gt;AWS&lt;/span&gt;
      &lt;meter min=&quot;0&quot; max=&quot;100&quot; value=&quot;55&quot;&gt;55%&lt;/meter&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/section&gt;</code></pre>
<pre><code class="language-css">.skills-list {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.75rem;
}

.skills-list li {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.375rem;
  background: var(--color-skill-bg);
  border: 1px solid var(--color-border);
  border-radius: 12px;
  padding: 0.75rem 0.5rem;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.skills-list li:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.skill-icon {
  font-size: 1.5rem;
  line-height: 1;
}

.skill-name {
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

/* Meter element styling */
.skills-list meter {
  width: 100%;
  height: 8px;
  border: none;
  border-radius: 3px;
  overflow: hidden;
}

/* Webkit (Chrome, Safari, Edge) */
.skills-list meter::-webkit-meter-bar {
  background: var(--color-border);
  border-radius: 3px;
  border: none;
}

.skills-list meter::-webkit-meter-optimum-value {
  background: var(--color-accent);
  border-radius: 3px;
}

/* Firefox */
.skills-list meter::-moz-meter-bar {
  background: var(--color-accent);
  border-radius: 3px;
}</code></pre>
<div class="callout callout-info"><strong>Why This Matters</strong><p><strong>CSS Grid vs Flexbox</strong> — Grid gives us a proper 4-column layout where all cards are the same width. Flexbox would work for wrapping pills, but Grid is better for equal-sized grid items. <strong>&lt;meter&gt;</strong> — A native HTML element for displaying scalar measurements within a known range. Screen readers announce it as a meter with a value. The text content (&quot;90%&quot;) is a fallback for browsers that don't support it. <strong>aria-hidden=&quot;true&quot;</strong> on skill icons — The emoji icons are decorative. Screen readers should announce the skill name, not &quot;snowflake&quot; or &quot;gear.&quot;</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>&quot;SKILLS&quot; heading with 8 skill cards in a 4-column grid. Each card has an icon, name, and colored proficiency bar. Hovering a card lifts it with a subtle shadow.</p></div>
<hr />
<h2>Step 7 — Fun Fact Aside</h2>
<p>I wanted a fun fact callout — content tangentially related to the main profile. <code>&lt;aside&gt;</code> is the semantic element for exactly this. Screen readers identify it as supplementary content that users can skip.</p>
<pre><code class="language-html">&lt;aside&gt;
  &lt;p&gt;
    I've contributed to 3 open-source projects
    with 500+ combined GitHub stars, and I spoke
    at ReactConf 2024 about building accessible
    component libraries.
  &lt;/p&gt;
&lt;/aside&gt;</code></pre>
<pre><code class="language-css">aside {
  background: var(--color-aside-bg);
  border-radius: 8px;
  padding: 1rem 1.25rem;
  font-size: 0.875rem;
  color: var(--color-text-body);
  border-left: 3px solid var(--color-accent);
}</code></pre>
<div class="callout callout-tip"><strong>When to use <aside></strong><p>Tangentially related content — a fun fact, a callout, a sidebar tip. The left accent bar (border-left: 3px solid) is a common visual pattern for callouts.</p></div>
<hr />
<h2>Step 8 — Contact Links (::before Icons + Attribute Selectors)</h2>
<p>I styled links differently based on their <code>href</code> attribute — no extra classes needed. The email link turns red automatically using <code>[href^=&quot;mailto:&quot;]</code>. I also added <code>::before</code> pseudo-elements with icons for each link type — email gets ✉, LinkedIn gets 💼, GitHub gets 💻, Twitter gets 🌐.</p>
<pre><code class="language-html">&lt;nav&gt;
  &lt;h2&gt;Contact&lt;/h2&gt;
  &lt;li&gt;
    &lt;a href=&quot;mailto:sarah.chen@example.com&quot;&gt;Email&lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a href=&quot;https://linkedin.com/in/sarahchen&quot;
       target=&quot;_blank&quot;&gt;LinkedIn&lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a href=&quot;https://github.com/sarahchen&quot;
       target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a href=&quot;https://twitter.com/sarahchen&quot;
       target=&quot;_blank&quot;&gt;Twitter&lt;/a&gt;
  &lt;/li&gt;
&lt;/nav&gt;</code></pre>
<pre><code class="language-css">nav ul {
  list-style-type: none;
}

nav li {
  list-style-type: none;
  margin-bottom: 0.5rem;
}

nav a {
  text-decoration: none;
  color: var(--color-text-body);
}

/* ::before pseudo-elements for link icons */
nav a::before {
  display: inline-block;
  width: 1.5em;
  text-align: center;
  margin-right: 0.25em;
}

nav a[href^=&quot;mailto:&quot;]::before {
  content: &quot;\2709&quot;;   /* ✉ envelope */
}

nav a[href*=&quot;linkedin&quot;]::before {
  content: &quot;\1F4BC&quot;;  /* 💼 briefcase */
}

nav a[href*=&quot;github&quot;]::before {
  content: &quot;\1F4BB&quot;;  /* 💻 laptop */
}

nav a[href*=&quot;twitter&quot;]::before {
  content: &quot;\1F310&quot;;  /* 🌐 globe */
}

/* Attribute selector: mailto links in accent color */
nav a[href^=&quot;mailto:&quot;] {
  color: var(--color-accent);
  font-weight: 500;
}

nav a:hover {
  color: var(--color-accent-hover);
  text-decoration: underline;
}

/* Focus: keyboard accessibility (WCAG 2.1) */
nav a:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: 2px;
}</code></pre>
<div class="callout callout-info"><strong>Why This Matters</strong><p><strong>::before for icons</strong> — No extra HTML or icon libraries needed. The icons are CSS-only using Unicode characters. <code>width: 1.5em</code> on the pseudo-element gives all icons the same width so the link text aligns vertically. <strong>Attribute selectors</strong> — <code>[href^=&quot;mailto:&quot;]</code> matches links starting with &quot;mailto:&quot;. <code>[href*=&quot;github&quot;]</code> matches links containing &quot;github&quot; anywhere. If you add another GitHub link later, it gets the icon automatically. <strong>:focus styling</strong> — Keyboard users navigate with Tab. Without :focus, they can't see where they are. This is a WCAG 2.1 accessibility requirement.</p></div>
<div class="callout callout-success"><strong>Checkpoint</strong><p>&quot;CONTACT&quot; heading with 4 links, each prefixed by an icon. Email is red with an envelope, others are gray with their respective icons. Hovering turns them dark red. Tab through them — you should see a red outline around the focused link.</p></div>
<hr />
<h2>Step 9 — Footer</h2>
<p><code>&lt;footer&gt;</code> isn't limited to the page bottom. It represents footer content for its nearest sectioning ancestor — here, the <code>&lt;article&gt;</code>. The card's copyright belongs inside the card's footer.</p>
<pre><code class="language-html">&lt;footer&gt;
  &lt;p&gt;
    &amp;copy; 2023 Sarah Chen. All rights reserved.
  &lt;/p&gt;
&lt;/footer&gt;</code></pre>
<pre><code class="language-css">footer {
  border-top: 1px solid var(--color-border);
  padding-top: var(--section-gap);
  margin-top: var(--section-gap);
  text-align: center;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}</code></pre>
<hr />
<h2>Step 10 — Card Hover, Animation + Mobile</h2>
<p>The finishing touches: a deeper shadow on hover (the transition was already set in Step 2), the <code>@keyframes fadeSlideUp</code> animation for the entrance, and responsive adjustments for mobile — the skills grid drops to 2 columns.</p>
<pre><code class="language-css">article:hover {
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.18);
}

@keyframes fadeSlideUp {
  0% {
    opacity: 0;
    transform: translateY(20px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (max-width: 767px) {
  body {
    padding: 1rem;
    align-items: flex-start;
  }

  article {
    padding: 1.5rem;
  }

  .profile-photo img {
    width: 120px;
    height: 120px;
  }

  .skills-list {
    grid-template-columns: repeat(2, 1fr);
  }
}</code></pre>
<div class="callout callout-info"><strong>Why This Matters</strong><p><strong>fadeSlideUp animation</strong> — The card starts invisible and 20px below, then fades in and slides up over 1 second. Applied in Step 2 via <code>animation-name</code> and <code>animation-duration</code>. <strong>align-items: flex-start on mobile</strong> — On short mobile screens, a vertically centered card gets cut off at both top and bottom. flex-start pins it to the top so users scroll down naturally. <strong>Grid columns: 2 on mobile</strong> — 4 columns are too tight on small screens. The grid drops to 2 columns for readability.</p></div>
<hr />
<h2>Step 11 — Dark Mode with :has()</h2>
<p>This is the CSS that powers the dark mode toggle I added in Step 3. When the <code>&lt;details&gt;</code> element is open, <code>body:has(details[open])</code> matches and swaps all the design tokens to dark colors. Every element that uses <code>var(--color-*)</code> updates automatically.</p>
<pre><code class="language-css">body:has(details[open]) {
  --color-bg: #0f0f1a;
  --color-card-bg: #1a1a2e;
  --color-text-primary: #e8e8f0;
  --color-text-body: #b0b0c8;
  --color-text-muted: #707088;
  --color-accent: #e94560;
  --color-accent-hover: #ff6b81;
  --color-skill-bg: #252540;
  --color-aside-bg: #20203a;
  --color-border: #2e2e4a;
}</code></pre>
<div class="callout callout-info"><strong>Why This Matters</strong><p><strong>CSS :has() selector</strong> — Often called the &quot;parent selector,&quot; <code>:has()</code> selects an element based on what it contains. <code>body:has(details[open])</code> means &quot;select body when it contains a details element that is open.&quot; This is a CSS-only state machine — no JavaScript, no event listeners, no state management. <strong>Custom properties cascade</strong> — Since every color references <code>var(--color-*)</code>, overriding the custom properties on body cascades to every descendant. One rule changes the entire theme.</p></div>
<hr />
<h2>Step 12 — Print Stylesheet</h2>
<p>The final touch — a print stylesheet that strips out decorative elements and makes the card printer-friendly. The dark mode toggle is hidden, shadows become borders, the photo is removed, and all text turns black.</p>
<pre><code class="language-css">@media print {
  details {
    display: none;
  }

  article {
    box-shadow: none;
    border: 1px solid #00000025;
  }

  .profile-photo {
    display: none;
  }

  aside {
    border: 1px solid #00000020;
  }

  nav a {
    color: black;
  }
}</code></pre>
<div class="callout callout-tip"><strong>Testing print styles</strong><p>In Chrome DevTools, press Ctrl+Shift+P (or Cmd+Shift+P on Mac) and type &quot;Rendering.&quot; Enable &quot;Emulate CSS media type: print&quot; to preview your print styles without actually printing.</p></div>
<hr />
<h2>Acceptance Criteria</h2>
<p>Here's the checklist I used to verify the build:</p>
<ul><li>Zero &lt;div&gt; tags in the HTML</li><li>Uses all semantic elements: &lt;main&gt;, &lt;article&gt;, &lt;header&gt;, &lt;details&gt;, &lt;section&gt; (×2), &lt;aside&gt;, &lt;nav&gt;, &lt;footer&gt;, &lt;figure&gt;, &lt;meter&gt;</li><li>Dark mode toggle works via :has(details[open]) — no JavaScript</li><li>Skills displayed in a 4-column CSS Grid with icons and &lt;meter&gt; proficiency bars</li><li>Contact links have ::before pseudo-element icons that align vertically</li><li>Card has fadeSlideUp entrance animation</li><li>Print stylesheet hides decorative elements</li><li>Hover effects on card and skill items</li><li>:focus styles on all links (keyboard accessible)</li><li>All values use CSS custom properties from :root</li><li>Responsive: skills grid drops to 2 columns on mobile, photo shrinks</li><li>Valid HTML — passes W3C Validator</li></ul>
<hr />
<h2>Quick Reference</h2>
<h3>CSS Units — When to Use What</h3>
<table><thead><tr><th>Unit</th><th>Relative To</th><th>Use For</th><th>Example</th></tr></thead><tbody><tr><td>rem</td><td>Root font size (16px)</td><td>Font sizes, spacing</td><td>font-size: 1.75rem</td></tr><tr><td>em</td><td>Element's own font size</td><td>Component-scoped padding</td><td>padding: 0.375em</td></tr><tr><td>px</td><td>Absolute</td><td>Borders, shadows, decorative</td><td>border: 1px solid</td></tr><tr><td>%</td><td>Parent dimension</td><td>Widths, radius, meter fill</td><td>width: 100%</td></tr><tr><td>vh</td><td>Viewport height</td><td>Full-page layouts</td><td>min-height: 100vh</td></tr></tbody></table>
<h3>CSS Selector Types Used</h3>
<table><thead><tr><th>Type</th><th>Syntax</th><th>Example</th></tr></thead><tbody><tr><td>Class</td><td>.name</td><td>.skills-list, .skill-icon</td></tr><tr><td>Descendant</td><td>A B</td><td>section h2, nav a</td></tr><tr><td>Pseudo-class</td><td>:state</td><td>:hover, :focus, :has()</td></tr><tr><td>Pseudo-element</td><td>::before</td><td>nav a::before for link icons</td></tr><tr><td>Attribute</td><td>[attr]</td><td>a[href^=&quot;mailto:&quot;], a[href*=&quot;github&quot;]</td></tr><tr><td>Adjacent sibling</td><td>A + B</td><td>section + section, aside + nav</td></tr></tbody></table>]]></content:encoded>
            <author>raviranjan7284@gmail.com (Ravi Ranjan)</author>
            <category>html</category>
            <category>css</category>
            <category>semantic-html</category>
            <category>flexbox</category>
            <category>css-grid</category>
            <category>responsive</category>
            <category>accessibility</category>
            <category>beginner</category>
        </item>
        <item>
            <title><![CDATA[JavaScript Event Loop: What I Got Wrong]]></title>
            <link>https://ravi-ranjan.in/articles/javascript-event-loop</link>
            <guid isPermaLink="false">https://ravi-ranjan.in/articles/javascript-event-loop</guid>
            <pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I thought I understood async JavaScript. I didn't. An interactive deep-dive into the event loop, microtasks vs macrotasks, and the mental model I had to unlearn.]]></description>
            <content:encoded><![CDATA[<h2>The Moment Everything Clicked</h2>
<p>I've been writing JavaScript for a while now. Async code, Promises, setTimeout — I use them almost every day. I thought I understood how they work.</p>
<p>Then I went down a rabbit hole.</p>
<p>I was watching Philip Roberts' talk <em>&quot;What the heck is the event loop anyway?&quot;</em> and somewhere around the 15-minute mark, something caught me off guard. I had always assumed the event loop was part of V8 — the JavaScript engine. Turns out, it's not. V8 doesn't know what an event loop is. It just executes code and manages the call stack. The event loop is the runtime's job — the browser's, or Node's.</p>
<p>That one thing unraveled a bunch of assumptions I didn't even know I was carrying.</p>
<p>Like — I thought <code>setTimeout(fn, 0)</code> runs basically immediately after the current code. It doesn't. I thought Promise callbacks and setTimeout callbacks were in the same queue. They're not. I thought I had a clear mental model of how async JS works. I didn't.</p>
<p>Here's what my mental model looked like before:</p>

<p>So I decided to actually sit down, go through the resources properly, and write it all out — because the best way I know to solidify something is to explain it.</p>
<p>This is that attempt. If we've had the same fuzzy understanding, hopefully this helps.</p>
<hr />
<h2>So — What Actually Is the Event Loop?</h2>
<p>Let me start with the thing that tripped me up first.</p>
<p>When I thought &quot;event loop,&quot; I thought &quot;V8.&quot; It felt like it should be part of the engine — the thing that runs JavaScript. But V8 is actually much more focused than that.</p>
<h3>What V8 Actually Does</h3>
<p>V8's entire job is to take our JavaScript, compile it, and execute it on the Call Stack. It also manages memory through the Heap. That's the full scope of what it does.</p>
<p>It doesn't know what <code>setTimeout</code> is. It doesn't know what <code>fetch</code> is. It has no concept of timers, network requests, or DOM events. If we ran V8 completely standalone, none of those APIs would exist.</p>
<h3>Where the Runtime Takes Over</h3>
<p>All of that comes from the runtime — the environment V8 is embedded in. In the browser, that runtime is managed by the browser itself (Chrome uses something called Blink alongside V8). In Node.js, the runtime is built on top of a C library called <code>libuv</code>, which handles all the async I/O and the event loop mechanics.</p>
<p>So the actual picture looks like this:</p>

<p>The Event Loop's only job is to watch the Call Stack. The moment it's empty, the Event Loop steps in and decides what runs next. And this is where the rule that governs all async JavaScript lives.</p>
<div class="callout callout-tip"><strong>The Core Rule</strong><p>Drain every microtask in the queue first → then pick one macrotask → check microtasks again → repeat.</p></div>
<p>Microtasks (Promise callbacks, <code>queueMicrotask</code>) always get priority over macrotasks (<code>setTimeout</code>, <code>setInterval</code>, I/O callbacks). Always. Even if the macrotask was queued first.</p>
<p>This is why this code does what it does:</p>
<pre><code class="language-javascript">console.log('1');

setTimeout(() =&gt; console.log('2'), 0); // macrotask

Promise.resolve().then(() =&gt; console.log('3')); // microtask

console.log('4');

// Output: 1 → 4 → 3 → 2</code></pre>
<p><code>setTimeout</code> with 0ms delay feels like it should run right away. But &quot;0ms&quot; just means &quot;queue it as soon as possible&quot; — it still goes into the Macrotask Queue. The Promise callback, being a microtask, jumps ahead of it.</p>
<p>I had definitely written code that depended on the wrong mental model of this ordering without realizing it.</p>
<hr />
<h2>Macrotasks and Microtasks — They're Not the Same Queue</h2>
<p>Once I understood that V8 and the runtime are separate, the next thing that clicked was that there isn't just one queue. There are two — and they have very different priorities.</p>
<p><strong>The Macrotask Queue</strong> is where callbacks from <code>setTimeout</code>, <code>setInterval</code>, and I/O operations land after their async work completes.</p>
<p><strong>The Microtask Queue</strong> is where Promise callbacks live — anything we chain with <code>.then()</code>, <code>.catch()</code>, <code>.finally()</code>, or write after an <code>await</code>. <code>queueMicrotask()</code> and <code>MutationObserver</code> callbacks go here too.</p>
<h3>Step-by-Step Tracer</h3>
<p>Let's trace through the example step by step. Click <strong>Play</strong> or step through manually:</p>

<p>Both setTimeout callbacks were queued before the Promise chain even resolved — yet the Promise callback ran first. Because microtasks always drain completely before any macrotask gets a turn.</p>
<p>And it's not just one pass through the microtask queue. If a microtask queues another microtask, that also runs before any macrotask. The event loop doesn't move to macrotasks until the microtask queue is <em>completely</em> empty.</p>
<h3>The Starvation Problem</h3>
<p>This has a consequence most people never think about. If a microtask keeps queuing another microtask, the macrotask queue never gets a turn:</p>
<pre><code class="language-javascript">// This will STARVE the macrotask queue
function keepGoing() {
  Promise.resolve().then(keepGoing);
}
keepGoing();

setTimeout(() =&gt; console.log('this never runs'), 0);</code></pre>
<div class="callout callout-warning"><strong>UI Freeze</strong><p>The setTimeout callback is permanently starved. The browser also can't render during this time because rendering happens between macrotasks. This would freeze the UI completely.</p></div>
<h3>async/await is Just Microtasks</h3>
<p>If we use async/await, we're already using microtasks — just with cleaner syntax. Everything after an <code>await</code> is the equivalent of a <code>.then()</code> callback:</p>
<pre><code class="language-javascript">async function run() {
  console.log('A');
  await Promise.resolve();
  console.log('B'); // this is a microtask
}

console.log('1');
run();
console.log('2');

// Output: 1 → A → 2 → B</code></pre>
<p>'B' doesn't run immediately after 'A' even though the Promise resolves instantly. The <code>await</code> suspends <code>run()</code>, lets the synchronous code ('2') finish, and then 'B' runs as a microtask. Once we see <code>await</code> as &quot;pause here, queue the rest as a microtask,&quot; the output becomes completely predictable.</p>
<hr />
<h2>Why Any of This Matters in Real Code</h2>
<p>Understanding the event loop isn't an interview exercise. I've run into each of these in actual projects — and each time, having the right mental model was the difference between a 5-minute fix and two hours of confusion.</p>
<h3>1. The Spinner That Never Spins</h3>
<p>This one is classic. We want to show a loading indicator before running something expensive:</p>
<pre><code class="language-javascript">setIsLoading(true);
const result = heavyComputation(); // takes 2 seconds
setIsLoading(false);</code></pre>
<p>The spinner never appears. We stare at it, add a console.log, confirm <code>setIsLoading(true)</code> is being called — and it is. So why doesn't it show?</p>
<p>Because the call stack never empties between those two lines. The browser only gets a chance to paint between macrotasks. Our heavy computation is blocking the single thread entirely, so the UI has no opportunity to reflect the state change.</p>
<p>The fix is to deliberately yield to the browser:</p>
<pre><code class="language-javascript">setIsLoading(true);
setTimeout(() =&gt; {
  const result = heavyComputation();
  setIsLoading(false);
}, 0);</code></pre>
<p>That <code>setTimeout(fn, 0)</code> isn't really &quot;run after 0ms&quot; — it's &quot;queue this as a macrotask, which lets the browser render first.&quot; Once we know how the event loop works, this stops being a weird trick and starts being an obvious tool.</p>
<h3>2. async forEach — The Silent Bug</h3>
<p>This one has caught a lot of developers off guard, including me:</p>
<pre><code class="language-javascript">[1, 2, 3].forEach(async (num) =&gt; {
  await fetch(`/api/${num}`);
  console.log(num);
});
console.log('done');

// 'done' logs first — forEach doesn't await anything</code></pre>
<p><code>forEach</code> doesn't know anything about the Promise our async callback returns. It calls the function, gets a Promise back, and immediately moves on — it never awaits it. Each <code>await fetch(...)</code> suspends that specific callback's microtask chain, but forEach has already finished by then.</p>
<p>If we need to wait for all operations:</p>
<pre><code class="language-javascript">await Promise.all([1, 2, 3].map(async (num) =&gt; {
  await fetch(`/api/${num}`);
  console.log(num);
}));
console.log('done'); // now this actually waits</code></pre>
<h3>3. Sequential await Is Silently Killing Our Performance</h3>
<p>This is less of a bug and more of a habit that costs us without realizing:</p>
<pre><code class="language-javascript">// Sequential — total time: 3 seconds
const user = await getUser();      // 1s
const posts = await getPosts();    // 1s
const comments = await getComments(); // 1s</code></pre>
<p>Each <code>await</code> suspends the function and queues the rest as a microtask after the Promise resolves. The next await doesn't even start until the previous one is completely done. If these three calls don't depend on each other, we're wasting time:</p>
<pre><code class="language-javascript">// Parallel — total time: 1 second
const [user, posts, comments] = await Promise.all([
  getUser(),
  getPosts(),
  getComments()
]);</code></pre>
<p>Same result, a third of the wait time. This only clicks properly once we understand that <code>await</code> is just a microtask checkpoint — not a magic &quot;go do this in the background&quot; instruction.</p>
<h3>4. Never Rely on setTimeout(fn, 0) Ordering</h3>
<p>A subtle one. It's tempting to write something like:</p>
<pre><code class="language-javascript">setTimeout(() =&gt; console.log('this should run first'), 0);
setTimeout(() =&gt; console.log('this should run second'), 0);</code></pre>
<p>In practice this usually works in order — but the moment we add Promises, or the browser is under any load, macrotask ordering between separate <code>setTimeout</code> calls isn't something we want to depend on architecturally. If order matters, make it explicit in our code structure, not in timing assumptions.</p>
<hr />
<h2>The One Thing to Take Away</h2>
<p>If I had to compress everything in this article into a single rule:</p>
<blockquote>When the call stack is empty — drain all microtasks first, then pick one macrotask, then repeat.</blockquote>
<p>That's the entire event loop. Every async quirk, every surprising output, every &quot;why isn't my spinner showing&quot; bug — it all traces back to that one rule.</p>
<p>But here's the deeper insight that changes how we think about JavaScript entirely:</p>
<p><strong>The JS Engine (V8)</strong> is single-threaded, synchronous, one thing at a time on the call stack. No exceptions.</p>
<p><strong>The Runtime</strong> — the Web APIs it provides (like <code>setTimeout</code>, <code>fetch</code>) actually run outside the JS thread entirely. The browser handles them in its own internal threads (written in C++). So when we call <code>fetch()</code>, the actual network request is happening in the browser's networking layer — completely separate from our JS thread.</p>
<p>So it's not that the runtime makes JS parallel — our JS code never runs in parallel with itself. What the runtime does is offload the <em>waiting</em> part (waiting for a timer, waiting for a network response) to the outside world, and only brings the callback back into our JS thread when the call stack is free.</p>
<div class="callout callout-tip"><strong>The Restaurant Analogy</strong><p>We (JS thread) place an order at a restaurant (Web API). We don't stand at the kitchen waiting — we go sit down and do other things. When the food is ready, the waiter (Event Loop) brings it to our table only when we're free (call stack empty). We're still only eating one dish at a time — but we didn't waste time just standing and waiting.</p></div>
<p>So the more precise conclusion is:</p>
<ul><li><strong>JS execution</strong> — always single-threaded and synchronous</li><li><strong>The waiting/I/O work</strong> — offloaded to the runtime, runs outside JS</li><li><strong>The result</strong> — feels asynchronous, but our code itself never truly runs in parallel</li></ul>
<p>Worth being precise about the word &quot;parallel&quot; — in interviews, that distinction matters a lot. And in debugging, it's the difference between understanding the bug and just staring at the screen.</p>
<hr />
<h3>What's Next</h3>
<p>This is the first article in a series I'm writing as I go deeper into JS engine internals. Next up: how V8 actually compiles our JavaScript — parsing, the AST, Ignition, TurboFan, and why the code we write affects how well V8 can optimize it.</p>
<p>If that sounds interesting, follow along — I'm learning this in public and writing it up as I go.</p>
<p><em>Find me on </em><a href="https://x.com/SquaredR7284">Twitter</a>, <a href="https://github.com/SquaredR98">GitHub</a>, or <a href="https://linkedin.com/in/raviranjan98/">LinkedIn</a>.</p>
<hr />
<h3>References</h3>
<p>These two talks shaped my understanding more than anything else I've read. If you haven't watched them yet, start here:</p>
<ul><li><a href="https://www.youtube.com/watch?v=8aGhZQkoFbQ">What the heck is the event loop anyway?</a> — Philip Roberts (JSConf EU 2014). The talk that started it all for me. He built <a href="http://latentflip.com/loupe/">Loupe</a>, an incredible visual tool to see the event loop in action.</li><li><a href="https://www.youtube.com/watch?v=eiC58R16hb8">JavaScript Visualized — Event Loop, Web APIs, (Micro)task Queue</a> — Lydia Hallie. Beautifully animated breakdown of microtasks vs macrotasks. Her visual style of explaining async JavaScript is unmatched.</li></ul>]]></content:encoded>
            <author>raviranjan7284@gmail.com (Ravi Ranjan)</author>
            <category>javascript</category>
            <category>event-loop</category>
            <category>async</category>
            <category>microtasks</category>
            <category>v8</category>
            <category>performance</category>
            <enclosure url="https://ravi-ranjan.in/blogs/OGImages/EventLoopMM.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Welcome to My Blog]]></title>
            <link>https://ravi-ranjan.in/articles/welcome-to-my-blog</link>
            <guid isPermaLink="false">https://ravi-ranjan.in/articles/welcome-to-my-blog</guid>
            <pubDate>Sat, 25 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[This is my space for sharing learnings, experiences, and everything I pick up along the way as a developer.]]></description>
            <content:encoded><![CDATA[<h2>Hey There!</h2>
<p>Welcome to my little corner of the internet. I'm Ravi, a full-stack developer who loves building things for the web. This blog is my space for sharing the learnings, experiments, and random discoveries that come with writing code every day.</p>
<h2>What This Space Is About</h2>
<p>I've always believed that the best way to learn is to share. So here I'll be writing about the things I work on, the problems I run into, and how I solve them — from frontend patterns to backend architecture, from debugging nightmares to deployment wins.</p>
<p>No gatekeeping, no over-engineering the words. Just honest notes from someone who's constantly learning and building.</p>
<h2>Let's Connect</h2>
<p>If you want to talk shop, have feedback, or just want to say hi — find me on <a href="https://x.com/SquaredR7284">Twitter</a>, <a href="https://github.com/SquaredR98">GitHub</a>, or <a href="https://linkedin.com/in/raviranjan98/">LinkedIn</a>.</p>
<p>More articles coming soon. Stay tuned.</p>]]></content:encoded>
            <author>raviranjan7284@gmail.com (Ravi Ranjan)</author>
            <category>introduction</category>
            <category>blog</category>
            <category>learnings</category>
            <enclosure url="https://ravi-ranjan.in/og-image.webp" length="0" type="image/webp"/>
        </item>
    </channel>
</rss>